5

I wrote an application with C++ / QT that communicates with a device to read/write its variables, puts/gets them in a struct and presents them in a gui for both viewing/editing purposes.

1) The device comes with a sample c code that also defines the communication protocol (in a very bad way) like:

#define VALUE_1 0x12345678
#define VALUE_2 0xDEADBEEF
#define MY_DEVICE_VAR (VALUE_1 << 4) & (VALUE_2 >> 6)
#define MY_DEVICE_VAR_1 (MY_DEVICE_VAR & (VALUE_1 << 2)
#define MY_DEVICE_VAR_2 (MY_DEVICE_VAR & (VALUE_2 << 4)
#define MY_DEVICE_VAR_2 (MY_DEVICE_VAR & (VALUE_2 << 4)
// .. and 300 more lines like above

So the variable VAR_1 is represented with: MY_DEVICE_VAR_1.

2) I've got a struct that holds all variables of the device:

struct MyDeviceData
{
    int var1;
    double var2;
    char var3;
    bool var4;
        .
        .
};

It's basically a storage for / a projection of the data read from the device. There are 4 different types of POD variables.

3) Finally my gui has gui elements to show and edit the instance of MyDeviceData

class MyGuI
{
    QLineEdit var1;
    QLineEdit var2;
    QComboBox var3;
    QCheckBox var4;
       .
       .
};

Now my questions are:

1) I'm doing the mapping of MY_DEVICE_VAR1 -> MyDeviceData::var1 -> MyGUI::var1 with if and switch/case statements which I'm not proud with. What would be a better "programmatic" way to do the mapping?

2) When the value of a gui element gets changed, I want to send only updated value to the card. besides overriding the handler functions of events like "textChanged, selectedIndexChanged" etc. Are there any "smarter" methods? (QSignalMapper?)

3) In this kind of project, is it possible to generalize whole drudge work? (a code-generator tool? templates?)

sithereal
  • 1,656
  • 9
  • 16

1 Answers1

2

I have recently faced exactly the same problem, although I was the one designing the device, its firmware, and the communication protocol as well.

I think that one must use model/view to keep one's sanity.

I have all the variables as elements in a data model class deriving from QAbstractTableModel. That's because there's a fixed number of simple parameters (rows), and they are the same per each device (column). Quite soon, though, I'll have to move to a tree model, because some parameters internally are structured as lists, vectors or matrices, and it'd be helpful to expose them to views directly as such, and not merely as formatted strings.

The model class also has some convenience getters/setters so that you don't have to refer to the parameters by their row/column. The row/column access via a QModelIndex is only for use by the views.

I chose to use the UserRole for directly represented values (mostly doubles in SI units), and Display and Edit roles to present formatted/scaled data to the widgets.

For non-view controls, one needs a binder object. QDataWidgetMapper is provided by Qt, and you should ideally use it.

A while ago I didn't notice that there was the widget mapper, so I wrote custom a binder object (deriving from QObject), that gets instantiated for each GUI control, to bind a certain index of the model to a non-view Qt control widget. Another approach would be to use QListViews, have a proxy model per each view that exposes just one element, and properly assign delegates. This would introduce a lot of overhead, though. The binder object approach is quite lightweight.

The model-view approach also enables one to easily factor out the up-to-dateness indication of each control.

  • When the application is first started up, the model can indicate (via a dedicated role), that the values are invalid. This can put an x-cross or barber pole on the control to clearly indicate that there is no valid value there.

  • When the device is active, and the user modifies a control, a different role can indicate that the value was changed in the model, but not propagated to the device yet.

  • When the device communications code picks up the change from the model and commits it to the device, it can tell the model about it, and the view (the biner, really) will automatically pick it up and update the control.

  • Adding a Model * clone() const method to the model, or adding serialization/deserialization operators, allows you to trivially take snapshots of the model, and implement Undo/Redo, Commit/Revert, etc.

Relevant snippets of the binder are here:

// constructor
Binder::Binder(QAbstractItemModel * model_, const QModelIndex & index_, QObject * object) :
   QObject(object),
   model(model_),
   index(index_),
   sourceRole(Qt::DisplayRole),
   property(""),
   target(object),
   lockout(false)
{
   Q_ASSERT(index.isValid());
   // replicate for each type of control
   if (qobject_cast<QDoubleSpinBox*>(object)) {
      connect(object, SIGNAL(valueChanged(double)), SLOT(doubleSpinBoxGet(double)));
      connect(index.model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(doubleSpinBoxSet(QModelIndex, QModelIndex)));
   }
   else if (....) 
}

// getter/setter for QDoubleSpinBox

void Binder::doubleSpinBoxGet(double val)
{
   if (lockout) return;
   QScopedValueRollback<bool> r(lockout);
   lockout = true;
   model->setData(index, val, sourceRole);
}

void Binder::doubleSpinBoxSet(const QModelIndex & tl, const QModelIndex & br)
{
   if (lockout) return;
   if (! isTarget(tl, br)) return;
   QScopedValueRollback<bool> r(lockout);
   lockout = true;
   if (! index.data().canConvert<double>()) return;
   qobject_cast<QDoubleSpinBox*>(target)->setValue(index.data(sourceRole).toDouble());
}

// helper

bool Binder::isTarget(const QModelIndex & topLeft, const QModelIndex & bottomRight)
{
   return topLeft.parent() == bottomRight.parent()
          && topLeft.parent() == index.parent()
          && topLeft.row() <= index.row()
          && topLeft.column() <= index.column()
          && bottomRight.row() >= index.row()
          && bottomRight.column() >= index.column();
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313