Model-View-Dialog generator for Qt
Repository: | download here |
Mailing list: | http://groups.google.com/group/qt-ham |
Qt's Model/View Programming might be extremely flexibly.
This flexibility comes at the price of tedious programming, even for simple case. What could be simple than having a struct filed with data, an array of such struct (either C++ array, or QList, or QVector, whatever), and a widget where you can see all those items and edit them.
A simple task, needed in many programs, no ?
Yet is is quite tedious to implement this in Qt 4.x:
- You have to hand-code an editor (Dialog) for it.
- You have to hand-code a model for it.
- You have to hand-code a view for it, or re-use QTableView.
mvg.py is a Model-View Generator and is a tool to solve this.
But first, we need some
Data representation
Suppose you want to store locations with some text. You could store them in a struct:
struct Locations {
QString name;
double longitude;
double latitude;
};
Field definition
Now is the time to download mvg.py.
Then create a file, say locations.yaml. In this file we describe this structure. It should be easy to parse for computers, and easy to read for humans. Sorry, no XML. Better use YAML.
locations.yaml could look like this:
Location:
fields:
- {
name: name,
type: QString,
}
- {
name: longitude,
type: double,
}
- {
name: latitude,
type: double,
}
- "-" is a YAML-element to denote array entries
- "{" and "}" are YAML-elements to denote blocks of data (hashes, if you know Perl or Python)
- "Location" is the name that our record will get
- "name" and "type" are attributes of the fields
mvg.py reads all of this and generate a file locations.h which now consists of
class Location {
public:
QString name;
double longitude;
double latitude;
};
You might know that struct and class in C++ are equivalent, It's just that by default all members of a class are private, while all members of a struct are public. And, by convention, structs normally don't have constructors and destructors. But they could.
Oh, what I wanted really say is that this is equivalent to struct Locations { ... }. It's just nicer for lazy people like my, because now I can write Location l;, otherwise I'd have been forced to write struct Location l;.
So far we haven't really saved work. But this will soon change, when we start with automatic
Field attributes
- "name": name inside of record (struct, class)
- "type": type of record. Currently, only a few types are allowed and
mvg.py will generate an exception if it encounters a type
that it doesn't know. That's usually easy to fix, mostly
mvg.py needs to know how to convert the type into a
QVariant or which
input widget it should use. Allowed types:
- "QString"
- "int", "bool", "quint32"
- "double"
- "head": Text to be used in the view header, or in the dialog
- "dia_label": optional. Normally the text from "head" would be used as dialog labels, but you can override this.
- "data_code": points to a C++ code snipped in the "data:" section of the YAML source file. This code snipped would return the field representation for the QAbstractItemModel::data() function.
- "sort_code": points also into a "data:" section, where a code snipped would implement sorting of this field type.
- "save": might be "true" or "false"
- "table_type": to override the widget that the view uses to display
a field. Allowed values:
- "checkbox": if a field is of type "bool" then a view would display a checkbox.
- "dia_type": to override the widget that the dialog uses to edit this field.
- "halign": to horizontally align a field in the view. Allowed values
- "left"
- "right"
- "center"
- "valign": dito, but vertically. Allowed values:
- "top"
- "bottom"
- "vcenter"
- "sort_order": initial sort order. Allowed values:
- "ascending"
- "descending"
- "readonly": makes this field a read-only field. You cannot change the field in the auto-generated dialog or in-place in the view.
- "default": default value for this field
- "default_code": pointer in a code snippet in the "code:" section that would return the default value.
Dialog code generation
A dialog doesn't just need the fields, but also labels for it. We can add "dia_labels" attributes to the fields, or we only add "head" attributes, which will be used for both the dialog and the view.
fields:
- {
name: name,
type: QString,
head: "&Name",
}
- {
name: longitude,
type: double,
head: "L&ongitude",
}
- {
name: latitude,
type: double,
head: "L&atitude",
}
Dialog definition
And then we add the description of some simple dialog:
dialog:
name: LocationDialog
head: "Location"
Now we run this new locations.yaml file through mvg.py. It will quickly create two files, locations.h and Locations.cpp. The dialog will look like this:
Let's first look at the header file:
#define LOCATIONS_H
// automatically generated from locations.yaml
#include <QString>
#include <QDialog>
class QLineEdit;
class QDoubleSpinBox;
class QFormLayout;
class QDialogButtonBox;
class Location {
public:
QString name;
double longitude;
double latitude;
};
class LocationDialog : public QDialog
{
Q_OBJECT
public:
LocationDialog(Location *record, QWidget *parent=0, Qt::WindowFlags f=0);
virtual void accept();
Location *m;
QLineEdit *editName;
QDoubleSpinBox *editLongitude;
QDoubleSpinBox *editLatitude;
QDialogButtonBox *okCancel;
QFormLayout *formLayout;
};
#endif
That's a simple dialog, with a field for every record and a pointer to the data-source.
In the generated locations.cpp seems also quite simple:
#include "locations.h"
#include <QLineEdit>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QDialogButtonBox>
// automatically generated from locations.yaml
LocationDialog::LocationDialog(Location *record, QWidget *parent, Qt::WindowFlags f)
: QDialog(parent, f)
, m(record)
{
setWindowTitle(tr("Location"));
resize(500, 250);
editName = new QLineEdit(this);
editLongitude = new QDoubleSpinBox(this);
editLatitude = new QDoubleSpinBox(this);
Here we create three edit widgets for our members.
formLayout = new QFormLayout;
formLayout->addRow(tr("&Name"), editName);
formLayout->addRow(tr("L&ongitude"), editLongitude);
formLayout->addRow(tr("L&atitude"), editLatitude);
We put them into a formLayout.
editName->setText(m->name);
editLongitude->setValue(m->longitude);
editLatitude->setValue(m->latitude);
Now we populate the edit widgets with data from our record.
And the rest is just the usual dialog boilerplate:
okCancel = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this)
connect(okCancel, SIGNAL(accepted()), this, SLOT(accept()));
connect(okCancel, SIGNAL(rejected()), this, SLOT(reject()));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(formLayout);
mainLayout->addWidget(okCancel);
setLayout(mainLayout);
}
Now the user can already ::exec() the dialog. If he presses "Ok", the following method will be called:
void LocationDialog::accept()
{
m->name = editName->text();
m->longitude = editLongitude->value();
m->latitude = editLatitude->value();
QDialog::accept();
}
... this method just stores the (eventually modified) data from the edit widgets back into the record.
Nothing surprising.
In fact, quite dumb --- once you know how to program this stuff.
And yet, mvg.py already saved me tons of typing. But it comes better. The next step is an extremely small step: let's have a container for records.
Dialog attributes
- "name": name of this dialog. You can, of course, generate several
- "head": head (or title) of this dialog
- "xsize": optional default size, in pixels, of this dialog.
- "ysize": optional default size, in pixels, of this dialog.
Container code generation
We now have a record, and we have a dialog to change the contents of the record. Now we want to store hundreds of those record somewhere. So we need a
Container definition
We add this to our locations.yaml file:
container:
name: locations
type: QList
This just adds the following line to locations.h:
extern QList<Location> locations;
That's not really a big achievement, but please consider that mvg.py also can generate code to load or save data into this container:
container:
name: locations
type: QList
load: true
save: true
... but this I won't cover here.
Container attributes
- "name": name of this container
- "type": type for the container, e.g. QList
- "load": TODO
- "save": TODO
- "load_code": TODO
Model code generation
Writing a model is sometimes quite subtle. If you google for modeltest.cpp, you'll even find test code that checks if your model behaves sane and doesn't crash.
mvg.py can save us from this tedious task. We need a
Model definition
We add another section to our locations.yaml file:
model:
name: LocationsModel
type: QAbstractTableModel
# This disables the automatically generated sort code of the model
sort: false
# This disabled editing directly inside the form:
edit_table: false
Now locations.h gained:
class LocationsModel : public QAbstractTableModel
{
Q_OBJECT
public:
LocationsModel(QObject *parent=0);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex() ) const;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role) const;
};
If we turn on sort and edit_table, it would even gain more:
// Sort support:
virtual void sort(int column, Qt::SortOrder order=Qt::AscendingOrder);
// In-Table edit support:
void store(const QString &sign, const QString &code);
Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole);
I'm sparing me to replicate here the contents of locations.cpp. Let me just say that it now now contains 161 lines. Most of them, if not all, aren't really breathtaking. But I now have made mgv.py generate 161 lines that I didn't had to code (and debug) by myself. And that is breathtaking :-)
You could already use this model with some other view, but mvg.py can do a view for you as well:
Model attributes
- "name": name of this model
- "type": type of this model. Currently mvg.py only supports QAbstractItemModel::data().
- "sort": optional, defaults to true: if sort-code for this model should be generated
- "edit_tablesort": optional, defaults to true, if in-place editing should be enabled. Please note that you define this in the model, but the actual in-place editing happens in the view. However, the view couldn't do that with model support.
View code generation
Now only the last piece is missing: conveniently generating a QTableView for us.
View definition
First we add some data to locations.yaml:
view:
name: LocationsView
type: QTableView
# This would disable the automatically generated sort code of the view:
#sort: false
delete: true
insert: true
This generates this header:
class LocationsView : public QTableView
{
Q_OBJECT
public:
LocationsView(QWidget *parent=0);
public slots:
virtual void keyPressEvent(QKeyEvent *event);
public slots:
void slotEdit(const QModelIndex &index);
};
And now you can use this like this:
model = new LocationsModel(this);
QLocationView view = new LocationView(this);
view->setModel(model);
view->show();
This is all you need to get this widget:
View attributes
- "name": name of this view
- "type": type of this view
- "dialog_name": optional name of the edit dialog, if not defined, the auto-generated dialog will be used.
- "dialog_include": optional include file for the above
- "insert": optional, defaults to false, if new records can be added via the insert key.
- "delete": optional, defaults to false, if records can be deleted via the delete key
- "sort": if on-the-fly sorting by clicking view headers, should be implemented