Même si certains développeurs s'en servent pour ça, la base de registres de Windows n'est à mon avis pas le meilleur endroit pour stocker des paramètres d'applications. La raison principale en est la quasi impossibilité de s'y retrouver ou de la sauvegarder dans un contexte de déplacement d'un logiciel d'une machine à une autre ou de réinstallation.
Bon, bien entendu ces inconvénients ont un côté pratique pour stocker "au hasard" des clés de licence pour un logiciel payant, mais est-ce vraiment toujours d'actualité ?
Dans un contexte de développement multiplateforme ou de potentialité d'évolution d'une application vers du multiplateforme, il faut tendre à utiliser au maximum la même base de code pour toutes les cibles. Ca diminue les pertes de temps de débogage et de maintenance. Il y a ainsi deux solution "simples" pour archiver des paramètres depuis Delphi : les fichiers INI à l'aide de l'unité IniFile et l'utilisation d'objects ou tableaux en format JSON.
L'utilisation des fichiers INI est un classique depuis la sortie de Windows 3 il y a une éternité. Pour JSON c'est quand même plus inhabituel mais ça a beaucoup d'avantages comme par exemple d'avoir un format exploitable dans différents langages de développements sur différents systèmes d'exploitation, en standard, alors qu'ils ne proposent pas de fonctions de gestion des .ini
Mais voilà, pour utiliser un stockage en format JSON sous Delphi ce n'est pas si aisé. Il faut se dépatouiller entre les TJSONValue, TJSONPair, TJSONObject, TJSONArray, TJSONBidule et TJSONTruc. C'est l'inconvénient d'un langage typé qui fait aussi tout son charme et sa sécurité.
Comme j'en avais besoin dans le cadre du développement d'une API, j'ai créé une classe me permettant d'utiliser des objets JSON comme espace de stockage de paramètres et les archiver automatiquement en fermeture "propre" de l'application.
Je vous livre ici les résultats de mon travail de cette nuit. Pas testée avec une application VCL mais elle devrait fonctionner comme avec n'importe quelle application Firemonkey.
unit uParam;
interface
type
tParams = class
class procedure save;
class procedure load;
class function getValue(key: string; default: string): string; overload;
class function getValue(key: string; default: boolean): boolean; overload;
class function getValue(key: string; default: integer): integer; overload;
class procedure setValue(key, value: string); overload;
class procedure setValue(key: string; value: boolean); overload;
class procedure setValue(key: string; value: integer); overload;
end;
implementation
uses
System.Generics.collections, System.IOUtils, System.SysUtils, System.JSON,
System.Classes;
var
paramChanged: boolean;
paramList: TJSONObject;
paramFileName: string;
function getParamsFileName: string;
var
folder: string;
filename: string;
app_name: string;
begin
app_name := TPath.GetFileNameWithoutExtension(paramstr(0));
folder := TPath.Combine(TPath.GetDocumentsPath, app_name);
if not tdirectory.Exists(folder) then
tdirectory.CreateDirectory(folder);
filename := app_name + '.par';
result := TPath.Combine(folder, filename);
end;
function getParamValue(key: string): TJSONValue;
begin
result := nil;
if assigned(paramList) then
if (paramList.Count > 0) then
result := paramList.getValue(key);
end;
procedure setParamValue(key: string; value: TJSONValue);
var
jsonvalue: TJSONValue;
begin
if not assigned(paramList) then
paramList := TJSONObject.Create
else if (paramList.Count > 0) then
begin
jsonvalue := paramList.getValue(key);
if assigned(paramList.getValue(key)) then
if (jsonvalue.value <> value.value) then
paramList.RemovePair(key).Free
else
exit;
end;
paramList.AddPair(key, value);
paramChanged := true;
end;
class function tParams.getValue(key: string; default: boolean): boolean;
var
jsonvalue: TJSONValue;
begin
jsonvalue := getParamValue(key);
if assigned(jsonvalue) then
result := jsonvalue.value.ToBoolean
else
result := default;
end;
class function tParams.getValue(key: string; default: string): string;
var
jsonvalue: TJSONValue;
begin
jsonvalue := getParamValue(key);
if assigned(jsonvalue) then
result := jsonvalue.value
else
result := default;
end;
class function tParams.getValue(key: string; default: integer): integer;
var
jsonvalue: TJSONValue;
begin
jsonvalue := getParamValue(key);
if assigned(jsonvalue) then
result := jsonvalue.value.ToInteger
else
result := default;
end;
class procedure tParams.load;
var
filename: string;
buffer: tStringStream;
begin
filename := getParamsFileName;
if tfile.Exists(filename) then
begin
if assigned(paramList) then
FreeAndNil(paramList);
buffer := tStringStream.Create(tfile.ReadAllText(filename, TEncoding.UTF8),
TEncoding.UTF8);
try
paramList := TJSONObject.Create;
paramList.Parse(buffer.Bytes, 0);
finally
buffer.free;
end;
end;
end;
class procedure tParams.save;
var
filename: string;
begin
if (paramChanged) then
begin
filename := getParamsFileName;
if assigned(paramList) and (paramList.Count > 0) then
tfile.WriteAllText(filename, paramList.ToJSON, TEncoding.UTF8)
else if tfile.Exists(filename) then
tfile.Delete(filename);
paramChanged := false;
end;
end;
class procedure tParams.setValue(key, value: string);
var
jsonvalue: TJSONString;
begin
jsonvalue := TJSONString.Create(value);
try
setParamValue(key, jsonvalue);
except
jsonvalue.free;
end;
end;
class procedure tParams.setValue(key: string; value: boolean);
var
jsonvalue: TJSONBool;
begin
jsonvalue := TJSONBool.Create(value);
try
setParamValue(key, jsonvalue);
except
jsonvalue.free;
end;
end;
class procedure tParams.setValue(key: string; value: integer);
var
jsonvalue: TJSONNumber;
begin
jsonvalue := TJSONNumber.Create(value);
try
setParamValue(key, jsonvalue);
except
jsonvalue.free;
end;
end;
initialization
paramChanged := false;
paramList := TJSONObject.Create;
tParams.load;
finalization
tParams.save;
if assigned(paramList) then
FreeAndNil(paramList);
end.
J'ai donc déclaré une classe tParams dans laquelle j'ai des méthodes de classe.
Pas besoin d'instancier cette classe dans mes applications : je n'ai besoin que d'un seul espace de stockage de paramètres, mais vous pouvez bien entendu adapter le tout s'il vous faut des objets de stockage différents et des actions à gérer en fonction de valeurs stockées.
Dans mon cas le chargement des paramètres est fait au lancement du programme et leur sauvegarde systématique se fait en fermeture du programme.
Par mesure de précaution, je recommande quand même d'appeler tParams.save lorsque vos utilisateurs sortent d'un écran de configuration, on ne sait jamais trop ce qui pourrait se passer, surtout sur un smartphone ou une tablette.
Notez aussi que je stocke le fichier de paramètres en clair, dans un dossier créé dans le dossier des documents de l'utilisateur.
Par mesure de précaution il vaudrait mieux crypter ces informations ou ajouter une information de contrôle de cohérence afin d'éviter toute modification manuelle du fichier. Surtout si vous y stockez des indicateurs liés à des achats inApp.
Une fois l'unité incluse dans le projet et utilisée en implémentation d'une fiche ou autre unité, il vous suffit d'utiliser directement les appels aux méthodes set et get, comme vous le feriez avec un fichier INI.
Pour enregistrer une informations :
tParams.setValue('app_token', edtAppToken.Text);
Pour lire une information :
edtAppToken.Text := tParams.getValue('app_token', '');
Dans les points importants à noter pour l'utilisation d'un objet JSON avec les librairies fournies par Embarcadero, il faut savoir que la lecture d'une arborescence plante lorsqu'il n'y en a pas. C'est pour cela que je teste systématiquement le nombre de composants de l'objet avant toute recherche/lecture d'une de ses propriétés.
if (paramList.Count > 0) then
result := paramList.getValue(key);
Je vous recommande vivement de faire pareil si vous voulez éviter d'éventuelles erreurs de violation d'accès en cours d'utilisation de vos programmes. La méthode getValue retourne la valeur désirée ou nil si elle ne la trouve pas, il faut donc ensuite s'assurer de ce qu'on a reçu avant de le retourner à l'appelant.
Autre point important à avoir en tête : on peut ajouter plusieurs fois des paires avec la même clé dans un TJSONObject. Par conséquent, lorsqu'on gère une modification il est préférable de supprimer la paire initiale si elle existe puis ajouter la nouvelle ou de modifier le TJSONValue associé au TJSONPair.
if not assigned(paramList) then
paramList := TJSONObject.Create
else if (paramList.Count > 0) then
begin
jsonvalue := paramList.getValue(key);
if assigned(paramList.getValue(key)) then
if (jsonvalue.value <> value.value) then
paramList.RemovePair(key).Free
else
exit;
end;
paramList.AddPair(key, value);
J'ai opté pour la première solution. C'est une façon étrange de procéder, mais au moins elle fonctionne à tous les coups.
Reste à voir dans quelle mesure l'abus de modification d'une information ne génère pas des zones mémoires à trous qui ne seraient pas réutilisables car trop petites selon ce qu'on stocke.
Je mettrai cette classe dans la boite à outils prochainement dispo sur mon compte GitHub. En attendant vous pouvez l'utiliser en la copiant/collant si vous le désirez. Et bien entendu si vous avez des questions ou suggestions, faites m'en part sans hésiter.
EDIT : Je vous recommande de lire également cet article concernant les directives de compilation et le conditionnement des blocs de code car il apporte une modification importante à la classe proposée ici. Bien entendu c'est la dernière version valide de la classe qui est disponible sur GitHub donc pas de soucis si vous la piochez dessus plutôt qu'en faisant un copier/coller de cette page.