Several older approaches to storing data related to program settings have been replaced by newer ones since the introduction of Delphi as the language kept pace with emerging modern ideas and practices. In general, examples of saving settings always followed the principles of the host operating system. This article looks into the various ways to store the settings for a cross platform app in the simple, universally accepted JSON format.
Table of Contents
Is saving settings different in a cross platform app?
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.
Delphi developers are spoiled for choice when it comes to using JSON
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
”.
For storing the MyProg program settings, let’s create a module MP.Settings and a class inside it.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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.
How do we store program settings in a cross platform way?
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
1 2 3 4 5 6 7 8 9 |
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.
How to add fields to the settings class?
Ok, let’s add fields to the settings class
1 2 3 4 |
public Chk1 : boolean; Chk2 : boolean; RadioIndex : integer; |
A constructor to indicate values of settings by default
1 2 3 4 5 6 |
constructor TMyProgSettings.Create; begin Chk1 := True; Chk2 := False; RadioIndex := 0; end; |
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
How to load cross platform app settings from a file?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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.
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 |
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; |
How do we actually use the LoadSettings method in our cross platform app?
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.
1 2 3 4 5 |
{ "chk1":true, "chk2":false, "radioIndex": 0 } |
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.
Is there a real cross platform app example of loading and saving settings?
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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.
How do we create a record to store our cross platform app settings?
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
TLoginSettings = record UserName: string; ShowRecent: Boolean; end; TMyProgSettings = class public Chk1: Boolean; Chk2: Boolean; RadioIndex: integer; Login: TLoginSettings; … |
Adding interface loading and saving methods to a login form
Let’s add interface loading and saving methods to the login form.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.
1 2 3 4 5 6 7 8 9 10 |
{ "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.
What are the differences between a Windows app and a cross platform app?
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>AppDataRoaming
.
It will be offered by the System.IOUtils.TPath.GetHomePath
method.
Let’s change the TMyProgSettings.GetSettingsFolder
method:
1 2 3 4 |
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 MacOS – 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.
1 2 3 4 5 6 7 8 |
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
ForceDirectories(Settings.GetSettingsFolder());
Here is a summary of what we learned in this article about saving settings to JSON
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.
You can download the source code for this article from the Softacom GitHub site here: https://github.com/SoftacomCompany/FMX.Settings-in-JSON
This article was written by Embarcadero Tech Partner Softacom. Softacom specialize in all sorts of software development focused on Delphi. Read more about their services on the Softacom website.
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition
There are a couple of errors in the code at the repo, which need to be corrected for it to compile and run.
Hi Chris, thanks for your comment and your suggested fixes (I edited out the actual fixes to make the comment more brief).
I have created a pull request against Softacom’s code and put in the fixes you mentioned. I have credited you in there – I didn’t know if you had a GitHub account or I would have linked to your directly.
Thanks once again.
My pull request with the fixes in it is here: https://github.com/SoftacomCompany/FMX.Settings-in-JSON/pull/1
Thanks Ian – it’s a great tutorial, and I’m most grateful for the insight and patterns provided.