sincerely
Singaporean
If you have not done so, read this full tutorial on how to use SGEXTN to build an application.
See here for the previous part of the tutorial.
In the previous part, we built the progress bar on the display page
Most applications will need to save user data across sessions. The dedicated SG_FileSystem module in SGEXTN makes this very convenient.
The user data in the case of Colours++ is the options that the user chose in the options page. These can be saved to a binary file using SGXFile. Although SGEXTN technically supports saving to a text file, support is not good and binary files occupy less space and are faster to read and write compared to text files containing the same information. The only reason why you would consider storing user data in a text file is if you are building a tool for software engineers who would need to write configuration files.
The file system can be easily accessed through SGXFileSystem, and SGXFile is used to read from and write to binary files.
We will implement a function that is called whenever "save current settings to preset" is clicked. For convenience, we will set the name of the preset to the current date. In a actual consumer facing app, it is likely better to let the user decide the name of files, but here we will leave out that to avoid being repetitive about UI.
Firstly, we must get all the information about what the user has selected from the UI.
But wait... We have already done that when we built the submit button, inside the function SGCLPOptionsPage::submitOptions. If we are to implement the same logic again in another function, we would have 2 copies of the same code.
Having 2 copies of the same code is known as code duplication.
This is bad because whenever you need to make a change, you need to do it to both copies, slowing down development speed. Furthermore if you only make a change to 1 copy because you forgot the other one, it can lead to weird bugs that are difficult to catch during testing.
To avoid this, we can define a new function in SGCLPOptionsPage
static bool checkOptions();
and refactor the code currently inside SGCLPOptionsPage::submitOptions into 2 functions. The part that handles reading the UI will go to SGCLPOptionsPage::checkOptions while the part only run when submitting the options should stay in SGCLPOptionsPage::checkOptions.
This is known as modularising code and is good for preventing code duplication.
bool SGCLPOptionsPage::checkOptions(){ if((*SGCLPOptionsPage::polygonSideCountInput).getInvalid() == true && ((*SGCLPOptionsPage::patternPolygonButton).getSelected() == true || (*SGCLPOptionsPage::patternStarButton).getSelected() == true)){ SGWNotify::pullDownNotify("invalid number of vertices chosen"); return false; } SGCLPOptionsPage::chosenForegroundColour = (*SGCLPOptionsPage::foregroundColourPicker).getColour(); SGCLPOptionsPage::chosenBackgroundColour = SGCLPOptionsPage::chosenForegroundColour; if((*SGCLPOptionsPage::backgroundUseCustomButton).getSelected() == true){SGCLPOptionsPage::chosenBackgroundColour = (*SGCLPOptionsPage::backgroundColourPicker).getColour();} else{ SGXColourHSLA backgroundColourHSLA(SGCLPOptionsPage::chosenBackgroundColour); if((*SGCLPOptionsPage::backgroundComplementaryHueButton).getSelected() == true){backgroundColourHSLA.invertHue();} if((*SGCLPOptionsPage::backgroundComplementarySaturationButton).getSelected() == true){backgroundColourHSLA.invertSaturation();} if((*SGCLPOptionsPage::backgroundComplementaryLightnessButton).getSelected() == true){backgroundColourHSLA.invertLightness();} SGCLPOptionsPage::chosenBackgroundColour = backgroundColourHSLA.toRGBA(); } if((*SGCLPOptionsPage::patternCircleButton).getSelected() == true){SGCLPOptionsPage::chosenPattern = SGCLPOptionsPage::Circle;} else if((*SGCLPOptionsPage::patternPolygonButton).getSelected() == true){SGCLPOptionsPage::chosenPattern = SGCLPOptionsPage::Polygon;} else if((*SGCLPOptionsPage::patternStarButton).getSelected() == true){SGCLPOptionsPage::chosenPattern = SGCLPOptionsPage::Star;} else{SGCLPOptionsPage::chosenPattern = SGCLPOptionsPage::Fractal;} SGCLPOptionsPage::chosenVertexCount = (*SGCLPOptionsPage::polygonSideCountInput).getTextAsInt(nullptr, 3, SGLIntLimits::maximum()); return true; } void SGCLPOptionsPage::submitOptions(){ if(SGCLPOptionsPage::checkOptions() == false){return;} SGWBackground::disable(SGCLPOptionsPage::instance); SGCLPDisplayPage::activate(); }
Once that is done, we can proceed to actually implement the saving of presets. Let us define a function SGCLPOptionsPage::savePresets that will be called when the button is pressed and adjust the line of code that creates the button to use the callback function ⁽㈳㈴㈳㈮㈱㈨㈠㈫ ㈧㈤㈱㈤⁾. You should already know how to do this.
static void savePresets();
Inside this function, obviously first we check that the UI is in a valid state and the user did not choose something nonsensical for the number of vertices.
if(SGCLPOptionsPage::checkOptions() == false){return;}
Note how we are reusing the same code.
Next we can determine the file path of the preset file. In this case, we can let it be
[user data folder] / [current time].sg
In code, that is
SGXString fileName = SGXFileSystem::joinFilePaths(SGXFileSystem::userDataFilePath, SGXTimeStamp::now().getFileNameCorrectToSecondSeparated('_') + ".sg");
Note a few things here.
Firstly, we are using SGXFileSystem::joinFilePaths to join the parent folder path with the file name. This is helpful as we would not need to write the / by hand which can be annoying.
Also, we are putting the file inside SGXFileSystem::userDataFilePath. This is the folder dedicated to storing user data. User data in this case refers to the documents that the app generates, but excludes app settings or configuration files. App settings or configuration files should instead go under SGXFileSystem::configFilePath.
We use SGXTimeStamp to get the current time as a string. Firstly SGXTimeStamp::now gives the current time as a SGXTimeStamp object. After that, SGXTimeStamp::getFileNameCorrectToSecondSeparated gives the string representation in a format compatible with the file system. This function also allows you to choose the separator used, which can be ' ', '-', or '_' depending on use case.
SGXTimeStamp converts everything to Singapore time zone, so you can either always use Singapore time zone in your app (recommended), or convert from the user's device time zone to Singapore time zone manually. In some places like Canadian Toronto, they like to randomly change the time zone for no reason at all. If you want to take that into account, you can use QDateTime to perform conversions properly.
Lastly note that the file extension is .sg, this is done for all SGEXTN application files that are not meant to be exported. In other words, the user will never actually see the file or need to manually transfer it somewhere. If the file needs to be exported, it is better to use a application specific extension.
Then we check if the file already exists. If it does, it means the user has been saving presets more than once this second. That likely indicates they are spamming the save button, so we ignore it.
if(SGXFileSystem::fileExists(fileName)){return;}
Here SGXFileSystem::fileExists is used to check if a file exists. There is also SGXFileSystem::folderExists to check if a folder exists. You may also find SGXFileSystem::getFilesList helpful, which gives a list of files in a folder of your choice.
After these checks, we can be sure that the preset needs to be saved and also whatever the user input is valid. We can then use SGXFileSystem::createFile to make a new empty binary file and then use SGXFile to write stuff into it.
SGXFile has a lot of commands for reading and writing a wide variety of data types. This includes both C++ stuff such as int and float, and also SGEXTN structs such as SGXColourRGBA and SGXIdentifier.
SGXFileSystem::createFile(fileName); SGXFile file(fileName, SGXFile::WriteOnly); file.writeColourRGBA(SGCLPOptionsPage::chosenForegroundColour); file.writeColourRGBA(SGCLPOptionsPage::chosenBackgroundColour); if(SGCLPOptionsPage::chosenPattern == SGCLPOptionsPage::Circle){file.writeInt(1);} else if(SGCLPOptionsPage::chosenPattern == SGCLPOptionsPage::Polygon){file.writeInt(2);} else if(SGCLPOptionsPage::chosenPattern == SGCLPOptionsPage::Star){file.writeInt(3);} else if(SGCLPOptionsPage::chosenPattern == SGCLPOptionsPage::Fractal){file.writeInt(4);} else{file.writeInt(0);} file.writeInt(SGCLPOptionsPage::chosenVertexCount);
Note that the enum is not written directly although technically enums can be casted to int. This is because if in future you want to add a new enum, the number that each enum currently corresponds to may shift. That would corrupt existing preset files. By manually converting them, you ensure that the code is future-proof and behaves consistently across compilers and devices.
For the same reason, 0 is written as the chosen pattern if the enum chosen is not any of the 4. Currently this is impossible, but future updates may add more enums and you may forget to update your code somewhere to handle these extra enums. In that case, having it write a invalid value makes the failure consistent and helps with debugging.
We can proceed to test the feature by clicking the button and checking if any file is created in the Documents/ColoursPlusPlus/yourdata folder. Assuming that you have done everything correctly, you should be able to find files with the extension .sg there, proving that the presets have actually been saved successfully.
See here for the next part of the tutorial.
©2025 05524F.sg (Singapore)