Monday, May 21, 2018

QLineEdit width cannot be set in terms of characters

Hopefully this will be a short post, but I can't believe how much time I've wasted on this, including digging into the QLineEdit source.

I used to use a GUI toolkit called Motif. It was written nearly 30 years ago. Of course it had a simple, single-line text editor widget. Like all widgets, you could set it's width and height, and it could be resized by managers (layouts).
There was a property you could set on the text widget to tell it how many characters it should display. This took into account the current font and always did a reasonable job of setting the width of the widget.

Fast forward nearly 30 years. I'm learning this shiny new Qt toolkit and want to set the number of characters to display in my QLineEdit. Nope, all you get is width in pixels. To fix this, you actually have to subclass QLineEdit and re-implement the sizeHint function:

QSize QLineEdit::sizeHint() const
{
    Q_D(const QLineEdit);
    ensurePolished();
    QFontMetrics fm(font());
    int h = qMax(fm.height(), 14) + 2*d->verticalMargin
            + d->topTextMargin + d->bottomTextMargin
            + d->topmargin + d->bottommargin;
    int w = fm.horizontalAdvance(QLatin1Char('x')) * 17 + 2*d->horizontalMargin
            + d->effectiveLeftTextMargin() + d->effectiveRightTextMargin()
            + d->leftmargin + d->rightmargin; // "some"
    QStyleOptionFrame opt;
    initStyleOption(&opt);
    return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h).
                                      expandedTo(QApplication::globalStrut()), this));
}
Notice the 17? That's how many characters you get. Silly programmer didn't you know 17 is always the right length?

Now that I know how stupid the code is, I can fix it, but a subclass is never a first class widget in the QtDesigner, so it will always be annoying, forcing me to add an extra function call for ever QLineEdit rather than just setting a property in QtDesigner.

Thursday, May 17, 2018

Did the Hightlighted row in a TableView change?

Seems like this would be one of the basic things you'd want to know about a TableView.
Granted the Qt TableView is insanely complex because it implements most of the functionality of a spreadsheet.
Maybe they should have had a simpler TableView and a TableEditor class.
Regardless, I am using a TableView as a fancy list display where you can sort on different columns and "select" the whole row.

So confusing point number one is that a TableView has a "current index" and a "selection" and they are not necessarily the same.

For example, this code changes both at once:

tableview->setCurrentIndex (index);

But this code, which appears to work, leaves a light blue "current index" indicator on the previously selected row if you sort the tableView first.

tableView->selectionModel()->select (index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);

As you can see from the above code, there are 2 levels to the API. This is annoying and confusing to say the least. Many operations can be done directly on the TableView, while others can only be done on the TableView's selection model. As shown above sometimes you can perform the operation on both, but with different results.

So back to the topic. I want to know what a row in my table is selected. Either when I user clicks on it with the mouse, or by using the cursor keys to run up and down the list.
I can do this to get clicks or double clicks:

void ActionList::on_tableView_clicked (QModelIndex index)

void ActionList::on_tableView_doubleClicked (QModelIndex index)
Usual Qt magic applies and if I get the name right these slots will automatically be connected. Kind of nice. Unfortunately, there is no selectionChanged signal on the TableView. There is one on the TableView's selection Model, but since it is an automatically created object whose name is "", you can't use the moc naming trick. Instead, you get to use this delight:

     connect (tableView->selectionModel(),
     SIGNAL (selectionChanged (const QItemSelection &, const QItemSelection &)),
      this,
     SLOT(selectionChanged (const QItemSelection &, const QItemSelection &)));
 At least it works, but it really should not be that convoluted.

Monday, May 14, 2018

Adding a new data element to a table and selecting it

I had no idea something so simple would take me so long to figure out. I want to have a menu item that adds a new data element to a table and selects it. See, sound easy, right?
Well, I also want to be able to undo it, which adds some complexity, but that's not the part that took me longest to figure out.

For one thing, there are a lot of objects involved in this. First you need a data model. I subclassed mine from the QAbstractTableModel since I'm going to display my data in a TableView. I really don't see much difference in the QAbstractTableModel and the QAbstractItemModel. I think they would work interchangeably for my case.

The TableView I created in Qt Designer, but since I want the table to be sortable and searchable, my data model needs a QSortFilterProxyModel. Right now I'm using the default one, but I think I'll need to create my own subclass of it when I get to the filtering part. So the constructor for my TableView looks like this:

ActionList::ActionList(QWidget *parent) :
QWidget(parent),
ui(new Ui::ActionList)
{
ui->setupUi(this);
proxy = new QSortFilterProxyModel(parent);
proxy->setSourceModel(actionModel);
proxy->setFilterKeyColumn(0);
ui->actionListView->setModel(proxy);
}

Not too bad, so far. Now I need a slot for my menu item. All it does is create an undo command and push it on the stack. The undo commands are really tedious because of all the object boiler plate required. I simplified mine somewhat by making the declaration and definition one in the same. This means I have to include the .cpp file into another .cpp file to compile it, which is ugly, but saves a lot of duplication. I'll see if it works out as the program gets more complicated.

undoStack->push (new AddActionCommand (actionModel, Action::Audio));

The QAbstractTableModel has a bunch of functions to add rows, columns and insert data, but I think these are all for edit-in-place tables. In my case the table is not editable, so I created a new function to add data in the form of my custom Action object.
QModelIndex ActionModel::addAction (Action *act)
{
int newRow = actions.count();
beginInsertRows(QModelIndex(), newRow, newRow);
actions.append(act);
endInsertRows();
return (index(newRow,0));
}

It stores the data inside the model and uses signals to alert the view to update the display. Note the return of the QModelIndex. It took me a long time to figure that out, and I'm still not sure it's the best way to do it but it allows me to select that ModelIndex in the View. Where it gets tricky is that the view may have been sorted, so the index(row) in the model is not necessarily the same in the view. Figuring out that I had to have a QModelIndex and map it between the original model and the proxy took me many hours. Finally making it work wasn't too bad, but even that has multiple ways to do it. So inside the undo command I do this:
AddActionCommand(ActionModel *model, Action::Type action_type, QUndoCommand *parent = 0)
: QUndoCommand(parent)
{
qDebug () << "AddActionCommand called"; action = new Action (action_type); actions = model; QModelIndex qmi = actions->addAction (action);

app->ui->action_list->setCurrentIndex (qmi);
setText(QObject::tr("Add thing"));
}

And the real magic happens in the actionlist:

ui->actionListView->setCurrentIndex (proxy->mapFromSource (qmi));
This all may be clear as mud since I left a lot out, but the summary is:
Keep track of your QSortFilterProxyModel
find the QItemIndex for your new data in the main model
use proxy->mapFromSource to turn your main model index into a proxy model index
finally call setCurrentIndex on your TableView with the proxy model index

Simple, right?

Friday, May 11, 2018

Signals and Slots

I'm going to start here, not because it's the first thing you need to know, but because I just lost most of a day getting bitten by Qt Creator's inconsistencies in this area.

On the surface I don't really see how signals and slots are significantly different that traditional callback functions, though the Qt docs claim they are better. Any time you start adding a language extension and another pre-compiler, I question the design. In this case I probably question the design of Qt and C++ equally.

Menus are special in Qt Creator. Instead of dragging and dropping components like you do for other things, they have a special editor right in the menubar and menus to let you add items and separators. When you create a menu item, you are actually creating a QAction that shows up in a special Action Editor window. If you right click on an action and select "Go to slot...", a helpful dialog will be displayed where you can pick the slot you are interested. Triggered is the default and most likely what you want. When you hit OK, a function and it's prototype are automatically created for you. This is very handy. I thought this also created some invisible link between the menu item and the function, but it does not. The link is simply the function name which must be of the form:
on_<widget name>_<signal name> ();
That's it, just name your function correctly, and the moc pre-pre-processor will connect it like magic.

Now here's where it gets weird, there is a special signal connection mode where you draw lines from one widget to another. If you are doing something simple like connecting a slider to a text box to display the value, it's great, you don't even have to write code. But if you need to actually write code to handle the signal, it gets a little confusing.

You can connect a signal to your window which looks like a ground connection on an electronics schematic. A dialog comes up that is similar to the "Go to slot..." one for menus. It lets you pick the signal to connect and you can define a new slot for it to connect to. I expected this to create the prototype and definition for the new slot automatically, but it does not. It really seems like a total waste of time, since you can just use the magical naming convention and not have to do anything in the UI.