Several approaches to storing data related to program settings have been replaced by newer ones since the introduction of Delphi. In general, they always followed the principles of a mother operating system.
Initially, INI files were widely used, then the use of a register was rather popular and then it became common to store data in files again. But due to some challenges, it was decided to use XML files instead of INI and then they were replaced with JSON.
Let’s have a look at the issues that a developer can face during the realization of settings storage. The methods that we will analyze in this article are not the only ones that are correct. But they have been already applied for several projects and have proved their efficiency in practice.
Looking ahead, we should note that the key peculiarity and the most pleasant part of the method is its automated serialization and deserialization of JSON objects in records and Delphi classes.
Modern Delphi versions offer two libraries for working with JSON. In this article, we will analyze the most widely used alternative open-source variant – X-SuperObject provided by a Turkish developer Onur YILDIZ
https://github.com/onryldz/x-superobject
Let’s start. In the folder with our original program texts, let’s create a folder Subrepos and execute there
git clone https://github.com/onryldz/x-superobject.git
We will see a folder x-superobject. Do not forget to add it to the Project — Options — Delphi Compiler — Search path “./subrepos/x-superobject”.
Initial realization
For storing the MyProg program settings, let’s create a module MP.Settings and a class inside it
123456789101112 | unit MP.Settings;
…
uses
…
XsuperObject;
TMyProgSettings = class()
public
end;
var
Settings : TMyProgSettings;
|
We have to choose where we will store a settings file. There can be a lot of variants, we will consider them later.
Now let’s make a simple decision and return to it later to make refactoring.
We are developing software for Windows and a commonly used platform today is Windows 64-bit. The development process is going on in an unprotected folder. That’s why we will store a settings file directly close to an exe file.
To ensure minimum refactoring, let’s introduce special methods for the class
123456789 | class function TMyProgSettings.GetDefaultSettingsFilename: string;
begin
Result := TPath.Combine(GetSettingsFolder(), 'init.json');
end;
class function TMyProgSettings.GetSettingsFolder: string;
begin
Result := ExtractFilePath(ParamStr(0));
end;
|
Now let’s turn to the main form of the app.
Thereby, let’s add fields to the settings class
A constructor to indicate values of settings by default
constructor TMyProgSettings.Create;
And the main content – methods of saving/loading data from a file.
Pay attention to the following moments:
By default the settings know in what file they are placed, a file name is an optional thing
serialization/deserialization take place in one line with helpers from XSuperObject
123456789101112131415161718192021222324 | procedure TMyProgSettings.LoadFromFile(AFileName: string = '');
var
Json : string;
begin
if AFileName = '' then
AFileName := GetDefaultSettingsFilename();
if not FileExists(AFileName) then
exit;
Json := TFile.ReadAllText(AFileName, TEncoding.UTF8);
AssignFromJSON(Json); // magic method from XSuperObject's helper
end;
procedure TMyProgSettings.SaveToFile(AFileName: string = '');
var
Json : string;
begin
if AFileName = '' then
AFileName := GetDefaultSettingsFilename();
Json := AsJSON(True); // magic method from XSuperObject's helper too
TFile.WriteAllText(AFileName, Json, TEncoding.UTF8);
end;
|
In the main form, the TMainForm.LoadSettings method will be responsible for settings loading. If a file doesn’t exist, it will be created and the default settings will be used.
Such methods as LoadFromSettings and SaveToSettings are responsible for transferring settings to the interface elements and vice versa.
1234567891011121314151617181920212223242526272829303132333435363738 | procedure TMainForm.LoadSettings;
begin
if Settings = nil then
Settings := TMyProgSettings.Create;
if not FileExists(Settings.GetDefaultSettingsFilename()) then
begin
ForceDirectories(Settings.GetSettingsFolder());
Settings.SaveToFile();
end;
Settings.LoadFromFile();
LoadFromSettings();
end;
// load UI components from settings
procedure TMainForm.LoadFromSettings();
begin
chk1.IsChecked := Settings.Chk1;
chk2.IsChecked := Settings.Chk2;
rb1.IsChecked := Settings.RadioIndex = 0;
rb2.IsChecked := Settings.RadioIndex = 1;
rb3.IsChecked := Settings.RadioIndex = 2;
end;
// Save UI components state to settings
procedure TMainForm.SaveToSettings();
begin
Settings.Chk1 := chk1.IsChecked;
Settings.Chk2 := chk2.IsChecked;
if rb1.IsChecked then
Settings.RadioIndex := 0
else if rb2.IsChecked then
Settings.RadioIndex := 1
else if rb3.IsChecked then
Settings.RadioIndex := 2;
end;
|
Now we need to call the LoadSettings method in the constructor of the main form and start the first variant of our program.
As we can see, the default values were applied, the interface is filled in according to the values from Settings.
Let’s have a look at the settings file. It is placed near the exe file and is named init.json.
If you call SaveToSettings() after changing every component and save Settings.SaveToFile() settings every time you close the program, the file content will change in accordance with your actions.
Contained objects
Let’s make our project a little bit closer to reality.
For opening a program, a user will have to enter a username and a password. Moreover, to make the interface more user-friendly, we will save the recent username.
Let’s create the simplest login form.
To the constructor of the main form,we will add a call for the following method
1234567891011121314151617181920212223 | procedure TMainForm.TryLogin();
var
F: TLoginForm;
Login, Password: string;
begin
F := TLoginForm.Create(NIL);
try
while not Application.Terminated do
begin
if F.ShowModal = mrOK then
begin
Login := F.edtLogin.Text;
Password := F.edtPassword.Text;
if LoginCorrect(Login, Password) then
Break;
end
else
Application.Terminate;
end;
finally
F.Free;
end;
end;
|
Pay attention to the fact that we are talking about a desktop platform where we have modal forms. For mobile apps, a login form will be supported in a slightly different way. But now we have another topic for consideration.
Let’s start a program and make sure that everything is working correctly. In the LoginCorrect method, we will write the right username and password and later it will be possible to conduct real testing.
Now let’s come back to the settings. In order to make our example more complicated, we won’t just create a new field in the settings but make it of a more complicated type, a record, and will store there a user’s choice on whether we need to show the recent login.
1234567891011121314151617181920212223242526272829 | TLoginSettings = record
UserName: string;
ShowRecent: Boolean;
end;
TMyProgSettings = class
public
Chk1: Boolean;
Chk2: Boolean;
RadioIndex: integer;
Login: TLoginSettings;
…
Let’s add interface loading and saving methods to the login form.
procedure TLoginForm.LoadFromSettings;
begin
edtLogin.Text := Settings.Login.UserName;
chkShowRecentUsername.IsChecked := Settings.Login.ShowRecent;
end;
procedure TLoginForm.SaveToSettings;
begin
if chkShowRecentUsername.IsChecked then
Settings.Login.UserName := EditLogin.Text
else
Settings.Login.UserName := '';
Settings.Login.ShowRecent := chkShowRecentUsername.IsChecked;
end;
|
And add method calls into a constructor and destructor, respectively.
We won’t need to change anything in the settings storage and saving and loading methods. Everything is working. Let’s check. Here you can see the saved settings file.
12345678910 | {
"chk1":false,
"chk2":true,
"radioIndex": 2,
"login":
{
"username":"admin",
"showRecent":true
}
}
|
Here’s what we will see during the repeated loading.
The program is working.
Details and cross-platform differences
At the start of realization, we’ve decided to store our settings file near the exe file. If our program is installed via an installer, it is highly possible that it will be placed somewhere inside the Project Files folder.
Access to the record of data there is prohibited by default, that’s why it is not a good idea to store there a settings file.
For these aims, we have the following folder C:\Users\<username>\AppData\Roaming .
It will be offered by the System.IOUtils.TPath.GetHomePath method.
Let’s change the TMyProgSettings.GetSettingsFolder method:
1234 | class function TMyProgSettings.GetSettingsFolder: string;
begin
Result := TPath.Combine(TPath.GetHomePath(), 'MyProg');
end;
|
Now on all platforms, the file will be saved correctly. On iOS and Android – in its own app data storage that is protected from other programs, on Windows – in the user’s data folder. On MacOC – in the user catalog /Users/<username> but it can be not very convenient that’s why it is possible to offer an alternative variant – for example, in the folder Library. On Windows, GetLibraryPath points to the app folder.
12345678 | class function TMyProgSettings.GetSettingsFolder: string;
begin
{$IFDEF MACOS}
Result := TPath.Combine(TPath.GetLibraryPath(), 'MyProg');
{$ELSE}
Result := TPath.Combine(TPath.GetHomePath(), 'MyProg');
{$ENDIF}
end;
|
As at the first start of the program, we didn’t have this folder, it is necessary to add to the first settings saving the call of
Let’s summarize what we have:
Pluses of this method
The described method allows adding data of a complex structure that are saved between the program starts without additional programming. Of course, it is necessary to write code where the data is used. But the code is simple and it is easy to support it.
Changes in a class structure automatically lead to changes in a file structure.
It is important that if some fields are not presented in a file or, vice versa, there are some extra fields, it doesn’t lead to errors in processing.
The JSON format is easy to read and it is simple to introduce changes to a saved file at the stage of debugging.
Minuses of the method
JSON is not intended for storing huge data arrays
It is not comfortable to store binary data, for example, uploaded images
On desktop platforms, a settings file is not protected from unauthorized viewing and updating
In general, all the mentioned problems have a solution that’s why the method considered in this article can be a very good choice.