Skip to content

Model view_binding

Paweł Salawa edited this page Jan 15, 2018 · 1 revision

What is a model-view binding concept?

A binding of widget(s) to the model lets you forget about transferring values from a variable to Qt widget and vice versa, every time the variable value changes, or user enters new value in the widget. If you change the variable programmatically, it's automatically reflected on the UI and if user enters new value in the widget, the value is automatically stored in the variable.

This is like having value visible in the Qt widget kept in a simple variable (like QString, int, bool, etc). Well, not really a simple variable - this framework wraps those variables in special classes with a "setter" and "getter" methods to control the data flow.

Why another binding framework?

Qt already serves Model-View mechanism with QDataWidgetMapper on board, but it lacks flexibility in regards of how the data model can be defined. To use QDataWidgetMapper one must provide QAbstractItemModel implementation, which has a "column/row" addressing concept. This is not really the most intuitive way to bind custom UI forms with the data.

With SQLiteStudio's framework for data binding you can define a set of key-value properties (where keys are made of group and key name, just like in INI files) and then you can tell which Qt widget should be linked to which key of the data model. This "telling" stuff is done from UI designer level - you just add new property to the widget and there you tell which key is linked to this widget. It's simple as that.

Note about the naming: As the framework was initially used only for ConfigDialog purpose, symbol names have much in common with "config" or "cfg", but the framework is not limited to the ConfigDialog only. This may also change in future.

Defining model

To define model you need to use macros from config_builder.h header. There are 3 major macros:

  • CFG_CATEGORIES(Name, Categories)
  • CFG_CATEGORY(Name, Entries)
  • CFG_ENTRY(Type, Name, DefaultValue)

Model should be defined in the header file. What those macros actually do is they define a C++ struct using provided definitions. This mechanism is enclosed in macros, so it's easier to write, understand and maintaint them.

Example

CFG_CATEGORIES(MyModel,
    CFG_CATEGORY(Category1,
        CFG_ENTRY(bool, Key1, true)
        CFG_ENTRY(bool, Key2, true)
        CFG_ENTRY(QString, Key3, "abc")
    ),
    CFG_CATEGORY(Category2,
        CFG_ENTRY(int, Key1, 1)
        CFG_ENTRY(QString, Key2, "xyz")
    )
)

This will create model with following entries (using a pseudo-code):

bool Category1.Key1 = true;
bool Category1.Key2 = true;
QString Category1.Key3 = "abc";
int Category2.Key1 = 1;
QString Category2.Key2 = "xyz";

Later on you will be able to refer them like this:

QString cat1key3Value = myModelInstance.Category1.Key3.get();
QString cat2key2Value = myModelInstance.Category2.Key2.get();

More operations on the model are available, but this will be covert later on. Keep reading.

Creating instance of the model

There are 2 ways to use the model objets:

  1. Create global singleton.
  2. Create local instances.

The first one is used mostly when the form is served to the ConfigDialog. All values configured in the ConfigDialog should be available throughout the entire application lifetime - after all, those are configuration options and we should have access to them at any moment. Global instances are persistable by default. This means that their state is stored in SQLiteStudio's configuration and are restored at next start.

The second one is used in any case, where the model data is not required to live all the time. A good example (and practiced heavily in SQLiteStudio) are plugins. Each plugin has its own model object defined as its class member. When the plugin instance is create, so is its data model. Then the plugin can provide its UI form (as a XML file) to SQLiteStudio and SQLiteStudio will create the form and bind plugin's data model with the created form. Local instances are not persistable by default. This means that once the model object is deleted, all its values are lost. This is useful when the model relates to some dialog windows that use the configuration model just for their life time - an example would be ImportDialog or ExportDialog.

The second approach has also another use case: you can use is for ConfigDialog when you want a plugin to be configurable. In that case your plugin should implement additionally the UiConfigurablePlugin class and return pointer to the member model instance. See API documentation for UiConfigurablePlugin for more details on that.

This way plugins can be independent from QtGui module, while still having possibility to use UI forms when SQLiteStudio runs in GUI mode.

Global instance

To define global instance named "MY_MODEL_INSTANCE" of the model defined as MyModel, put this into header:

#define MY_MODEL_INSTANCE CFG_INSTANCE(MyModel)

and this into the cpp file:

CFG_DEFINE(MyModel)

From now on you can access the MY_MODEL_INSTANCE throughout the entire applications lifetime, like this:

int currentSpinValue = MY_MODEL_INSTANCE.Category2.SpinKey.get();

It's a good idea to use shorter, more handy names, like:

#define MYMODEL CFG_INSTANCE(MyModel)

which will result in shorter usage:

int currentSpinValue = MYMODEL.Category2.SpinKey.get();

You can also use "lazy" global instance, which is not crated automatically at the application startup, but instead it's created when it's first time requested. To define such instance, change CFG_DEFINE in your cpp file to:

CFG_DEFINE_LAZY(MyModel)

Lazy instances are essential for config structures that use GUI classes as default values (for example CFG_KEY_ENTRY uses QKeySequence), because calling constructor of some GUI classes at application startup time will cause the application to crash. That's Qt's limitation.

Local instance

If you want to create the model as a member field of some class, you can do it using CFG_LOCAL macro in that class definition, like this:

class MyClass
{
    // ...
    private:
        CFG_LOCAL(MyModel, modelObject) // note that this macro does not expect semicolon at the end
};

You can use it in the class methods like this:

MyClass::setKey1(bool value)
{
    modelObject.Category1.Key1.set(value);
}

Persistable local instance

If the local instance is to be used for UiConfigurablePlugin, then the instance should be declared as persistable, even it's a local instance. Since plugin is a singleton, there will also be only single instance of the model to persist. To declare persistable local instance use CFG_LOCAL_PERSISTABLE macro:

class MyClass
{
    // ...
    private:
        CFG_LOCAL_PERSISTABLE(MyModel, modelObject)
};

Defining keys for widgets

In order to bind model to widgets, you need to define which widgets are linked with which model keys. This is done by adding dynamic property named "cfg" to the widget.

Example

Have a following form prepared in Qt Designer:

Select first checkbox:

Add a String type of dynamic property:

Put category and key of the model entry, separated by a single dot:

Now create a model which can store a data from this checkbox. This goes to the header file:

#include "config_builder.h"

CFG_CATEGORIES(MyModel,
    CFG_CATEGORY(Category1,
        CFG_ENTRY(bool, Key1, true)
    )
)

// ... your class definition

You can do the same for all other widgets from the form, then update model definition to look like this:

#include "config_builder.h"

CFG_CATEGORIES(MyModel,
    CFG_CATEGORY(Category1,
        CFG_ENTRY(bool, Key1, true)
        CFG_ENTRY(bool, Key2, false)
        CFG_ENTRY(QString, RadioKey, "radio 1")
    ),
    CFG_CATEGORY(Category2,
        CFG_ENTRY(int, SpinKey, 10)
        CFG_ENTRY(QString, LineKey, QString())
        CFG_ENTRY(QString, TxtKey, "some\nmultiline\ntext")
    )
)

// ... your class definition

QRadioButtons

Radio buttons can provide different values for the same model entry.

We have only 1 model entry for 2 radio buttons. That's because we want them to exclude each other. This is done by defining the same value for "cfg" property for both those radio buttons.

Two extra things need to be done for both QRadioButton to work properly:

  1. Change widget class from QRadioButton to ConfigRadioButton. To do so, right-click on the radio button and use "Promote to ...". As promoted class name put ConfigRadioButton. As header file put commoon/configradiobutton.h. Press Add and then Promote.
  2. Add one more dynamic property to each radio button. The property has to be of the same type you chose for the radio button in the model - in our example it's a String (for QString). Name the property "assignedValue". For one radio button put "radio 1" and for another put "radio 2". One of radio buttons should always have the same value as defined default in the model, otherwise no radio button will be selected by default.

Final effect:

See HtmlExport plugin source code to see real life example.

QComboBox

Combo boxes are supported out of box, although it is possible to make list of combo box values model-driven using a CFG_ENTRY entry with QStringList data type. This way you can change values in the config list and it will be reflected in the combo box value list.

In order to apply this you need to do 2 things:

  1. Change widget class from QComboBox to ConfigComboBox. It's done the same way as for QRadioButton, see above. The header name is: commoon/configcombobox.h
  2. Add one additional dynamic property of type String, named "modelName". As the value of the property put the category and key (just like for "cfg" entry) pointing to the configuration entry that holds the QStringList.

From now on, every time you change the configuration entry with QStringList, the combo box will update its values.

If modified value list contains the most recent selected value of combobox, than that value is restored after model update. If not, then the currently selected value of combo box is set to none.

See PdfExport plugin source code to see real life example.

Bind a model object to a form

Once you have a model and a form with defined keys it's time to put those two together. If you are just implementing some plugin and want to provide the UI form (like for example ExportPlugin), then you don't need to care about this. This has to be done only if you are implementing a new place, where the UI forms will be loaded dynamically by SQLiteStudio.

For existing plugin types, such as ExportPlugin, ImportPlugin, PopulatePlugin and others - this is already implemented. If you do need to know how it's done, keep reading.

The simplest case is done by a single method call from class ConfigMapper. Let's have a look at the full example:

Example

Header file:

#include "config_builder.h"

CFG_CATEGORIES(MyModel,
    CFG_CATEGORY(Category1,
        CFG_ENTRY(bool, Key1, true)
        CFG_ENTRY(bool, Key2, false)
        CFG_ENTRY(QString, RadioKey, "radio 1")
    ),
    CFG_CATEGORY(Category2,
        CFG_ENTRY(int, SpinKey, 10)
        CFG_ENTRY(QString, LineKey, QString())
        CFG_ENTRY(QString, TxtKey, "some\nmultiline\ntext")
    )
)

class MyClass : public QWidget
{
    // ...

    private:
        ConfigMapper* configMapper;
        QWidget* myForm;
        CFG_LOCAL(MyModel, cfg)
};

Cpp file:

// ...

MyClass::MyClass()
{
    QUiLoader uiLoader;
    myForm = uiLoader.load("myForm.ui", this);
    configMapper = new ConfigMapper(&cfg); // <-- here we create mapper (binder) and we give it the model object pointer
    configMapper->bindToConfig(myForm);    // <-- this is the essential call
}

MyClass::~MyClass()
{
    delete configMapper;
}

The ConfigMapper class has much more features. To learn them all see the API documentation for that class.

Working with model

Each model entry is actually an instance of CfgEntry class, which is a wrapper around the simple data type you provided when declared it with CFG_ENTRY() macro.

Following methods from that class are essential:

  • set()
  • get()

There are more methods, but those are two that you will be using in most cases. Their purpose is obvious. Here's how you use them:

QString value1 = cfg.Category1.RadioKey.get();
cfg.Category1.RadioKey.set("radio 2");

Persistable vs transient models

Each defined model can be instantiated as a persistable or transient model object.

Persistable objects are managed by SQLiteStudio. It saves all values from that object into SQLiteStudio's configuration file and those values are restored at the next start.

Transient objects lose their state once they are deleted. When created again they have a default values, as defined in model.

Global model instances are persistable by default and local model instances are transient by default. Currently only global instances can be configured in this regard. Local instances are always transient. Configuration is done by using a proper macro in a cpp file, when you define a global instance:

CFG_DEFINE(MyModel) // model object will be persistable
CFG_DEFINE_RUNTIME(MyModel) // model object will be transient

Creating new widget binders

By default model-view binding framework supports simple data types and simple widgets, but sometimes we want some complex object to be reflected in a complex widget, like QTableWidget.

For this purpose there is a CustomConfigWidgetPlugin interface. It allows developer to define exactly how the object of given type should be read from or written to the given widget type.

Some examples of existing custom widget binders to learn from (all in SQLiteStudio3/SQLiteStudio/common/):

  • ComboDataWidget
  • ListToStringListHash
  • StyleConfigWidget

Extra features

Config keys with custom title

Every CfgEntry can have a title (for example shortcut config entries use it). It can be defined at config model definition stage by passing one more argument to the CFG_ENTRY macro, a string argument. Later on the same string will be returned by CfgEntry::getTitle().

Notifable config widgets

By default the ConfigDialog loads all forms (including config forms from plugins), loads current configuration into those forms and user can change values on UI, but the actual configuration model is not changed until user clicks "Apply" or "Ok". This is because ConfigDialog doesn't use binding for config model, it just loads and stores it.

Unfortunately sometimes it's useful to know, when the user changes anything on the UI, even it's not stored in the config model yet. An example would be code formatter plugin, which might want to preview the code formatted with configuration being changed by the user on the fly.

For such cases you can define a dynamic property to form widgets, that should notify your plugin about being changed. The property should be named "notify" and it should be of boolean type. Enable it, to make this feature work. You have to do this for every widget that you want to notify about.

Second thing is to have your plugin inherit ConfigNotifiablePlugin abstract class and implement its method (configModified()). Every plugin that implements this interface will be called whenevery any of notify-marked widgets is modified.

Your configModified() implementation should check whether the CfgEntry instance passed as an argument is the one you want to handle in any way in your plugin. Just compare it with your plugin config model (compare pointers), so you know that this is the config you're interested in.

Note that when configModified() is called, it has a CfgEntry and QVariant arguments passed, because the config entry is just to let you know which configuration parameter was modified by the user on UI and the second argument is the new value of that parameter. The new value of the parameter is not stored in the config entry yet, that's why it's passed to you in the additional argument.

Preview config widgets

For the same reason as above, the configuration model is loaded to ConfigDialog and then any changes to the configuration model are not reflected dynamically in the dialog. You can enforce updating specific configuration widgets, just like they were connected with binding, by adding dynamic property to the desired widget. Property name should be preview and it should be of boolean type. Enable it to make this feature work.

From now on, every time you change the configuration entry, the UI widget will be updated to reflect it's value.

As before, this also comes handy when for example you need to show how the code formatter formats the code using current settings. You can just have a config model entry that holds the code to display and link it with the text widget in config form. Having the text widget with "preview" property enabled, you will be able to set the widget contents by simply setting the config model entry.

Why is this so complicated, you ask? It's because this way we have a model classes that are independent from UI and your plugin is dependent only on those classes, not on QWidget, not on GUI. You are still able to "talk" to GUI, but your plugin is not dependent on GUI, so it can still be loaded in the CLI mode.

Again, this matters only to ConfigDialog. In other cases you will usually use ConfigMapper::bindToConfig() to have such behaviour enabled across the board.

Clone this wiki locally