[SOLVED] QTableView, editing policies, insertRow() and a few more



  • Hello.

    Please, take this one with a grain of salt. I am trying to improve over something I've been working on, and that's not working the way I'd like it to. I am having a bit of a problem trying to polish this.

    I have a QTableView where I am listing the items for a budget. Each record has a number of fields, some of them are editable, some are read-only. The first [visible] field is the item description. Then some other fields follow such as quantity, price, subtotal, and a couple more.

    I am trying to design a comfortable editing scheme for the final user; taking into a account that the items are stored in a database and that the elements on the budget are to be taken from that database the logical way to proceed, as I see it, is to use a completion delegate of some sort to fill up the first field, then, on tab skip to the quantity field, then on tab go to the next line where a new row should be automatically inserted and the completion delegate should display again so the user can fill in one more item into the budget. And so on ad infinitum. :)

    So far, the big picture is working, but with some rough edges. And I wonder, what a Qt specialist would do in this situation.

    So far, what I am doing right now is more or less like this:

    • I have set setEditStrategy(QSqlTableModel::OnManualSubmit) so I can control the thing myself.
    • The whole thing starts when I click a QToolButton, then on_tbtn_addToBudget_clicked() is called. This slot runs insertRow() in the model at rowCount() (so it adds a new line at the end of the model). It also runs scrollToBottom() to ensure that the user is seeing what he's editing (I'd appreciate some hint on a proper way to do this), and runs edit(QModelIndex(row, column)) so that the description field is open for edition using the assigned QStyledItemDelegate.
    • I have connected primeInsert() from the model to a self-made prepareRecord() function, that mostly assign 0 values to the price and subtotal fields, 1 to quantity, and the current budget ID number to some other field that's not visible.
    • I have connected dataChanged() from the model to a function (let's call it onDataChanged()) that, amongst many other things, sets the right values for all the fields when the description field changes. It also adjusts the subtotal when quantity changes, and does some other things based on the value of a percentage field that can also be edited.

    So, this mostly works, but...

    • I'd like to know if there's a better approach to adding lines. Right now I have to insert them without previously knowing if the contents is valid. If the user interrupts the editing in some way (i.e. just pressing Esc) a line with blank (or partial) description and some fields with 1's and 0's onto them will remain. If this is done a few times you can see them piling in the bottom. I've tried conditionally removing them based on contents from onDataChanged(), but it seems to ignore the removeRow() sentence altogether. Maybe because it's not really in the model, just in the view. The fact that restarting the app clears those lines seems to confirm that. How do you, guys, handle this kind of situation where you have to validate records? Do you submit them temporarily as I do so they appear in the view? or do you, maybe, use some other kind of mechanism?
    • I really need to find a sane way to control the way this works using the keyboard. At this point I am a bit confused on where should I be doing this. I want tab and enter to behave exactly the same. If they are pressed on description, skip to quantity, if pressed in quantity, open a new line and edit another description. I guess the right place for this is the delegate, but, since I have a QLineEdit based completer in the description column, which, in turn, displays a lot of options into a QListView, maybe for that field I will have to control that in the QListView?

    Sorry for the long post, but I really need to iron this out more than I need guidance with concrete code. That's why I am not posting code snippets (this is long enough). If you need to take a look I can post them of course.

    Thanks for reading and for any guidance :)



  • Hi,
    In respect to your tab/enter use, optimize the tab sequence number to get this in 'proper' order. Your thinking is great to no only support mouse clicking etc.
    For the use of the delegate I'm not an expert if an delegate may also be used for multiple cells.
    What I would try is to get the clicked signal (if on a empty row, of maybe a (+) button) and start a QWidget designed dialog. In here the user must insert all fields and only then the OK button becomes active. The Cancel will be there of course. When OK is done, send the data to your model. The model may then signal the update to the View.
    The Dialog is a class of its own inherited QDialog or QObject and when made Model it must be completed or cancelled by the user. On all items you are able to set InputFilters etc.
    This will keep your model clean of unwanted inserted items. That's what I would try to do.



  • Thanks for the suggestions, I have considered this, but the program is intended to be used as a replacement for something else that works the way I described, just directly in the table itself. Similarly to speadsheets but not quite the same. Putting a dialog in the middle would make the editing slower, more and more taking into account that a budget can rapidly grow over a couple thousand rows.

    The delegate I am using checks the column number, and sets the editor according to that. The description column for example uses the lineedit widget with a dropdown tableview as a completer. The quantitly uses a spinbox, and so on...



  • Oke,
    So basically your stuck with a GUI that isn't great to work with. The way you described how it works (for your intended use) sounds to me the best solution, setting the delegate for every column individually. IYAM a dialog for new items isn't slower the direct altering cells. You could also make both available. For new/empty items open a dialog, for known cells use the delegates. That sounds to me as the quickest and most reliable GUI.
    think your ideas are correct and shouldn't need much changing in working.



  • Hi,

    I think instead of edit(QModelIndex(row, column)) you should use openPersistentEditor ( const QModelIndex & index ) , and connect the item delegate's signal closeEditor(QWidget*) to a slot where you can implement custom behaveiour for what's next depending on what key has opened it; this signal is emitted when press return or escape while editing

    as regarding data validation I think you should do this in item delegate setModelData(...) where you handle each widget editor as in createEditor etc. and in closeEditore slot based on the validation's result you can manage removeRow ...

    hope this help!

    Cheers!



  • [quote author="NicuPopescu" date="1381321082"]Hi,

    I think instead of edit(QModelIndex(row, column)) you should use openPersistentEditor ( const QModelIndex & index ) , and connect the item delegate's signal closeEditor(QWidget*) to a slot where you can implement custom behaveiour for what's next depending on what key has opened it; this signal is emitted when press return or escape while editing

    as regarding data validation I think you should do this in item delegate setModelData(...) where you handle each widget editor as in createEditor etc. and in closeEditore slot based on the validation's result you can manage removeRow ...

    hope this help!

    Cheers![/quote]

    Indeed it helps. I have to review your post carefully, this will take me some time hehe, but I suspect this will simplify the whole thing quite a bit. I'll let you know how it goes.

    Thank you :)



  • Surely it's something silly, but I am having a hard time understanding how to implement the <to-next-field> thingie on my onCloseEditor(QWidget*) slot that you mentioned.

    Since the editor is triggered by the closeEditor(QWidget*) function, and hence, it will receive a pointer to the widget as an argument, I haven't a clue on how to skip to another given index or open a new line.

    I've read about the recommended action hint, but that won't be sufficient since, as I said, some fields are to be skipped even if they are visible in the view.

    Still reading and googling hehe!



  • you need to know where you are in table view, which cell, right? and the widget editor is helpless ... the workaround is to pass and keep a reference to table view in your subclassed QStyledItemDelegate, even I think is closeEditor slot, the app's focusWidget should be the table view and you could cast it

    Cheers!



  • Mmmm, this is turning interesting hehe. Well, my QStyledItemDelegate already receives the table view as the first argument in the constructor, so it's just a matter of making it global to the class (or just pass it to that concrete function). It also receives a QSqlQueryModel, which is used for the completer that there's in the description field. :)

    Thank you for all the guidance. Still working on this as time permits, I'll post back.

    :)



  • It's truly amazing how well things work when you do them in the right place (as opposed doing to doing them in the wrong place).

    The performance is lightyears away, plus some bugs simply went away. Additionally I took rid of some blockSignal()'s that I had around just so that the previous implementation wouldn't iterate over and over due to dataChanged() firing up several times on each row :lol:

    Up to now, I've only connected the closeEditor() signal to my own function, where I do some object casting so that I can reach the right members in the right structures, and then do all the field magic.

    I didn't need to change much more, though I still haven't gotten into implementing the keyboard sequence. I'll do that tomorrow, probably.

    NicuPopescu, or someone else who know the answer, could you hint me onto the right direction in understanding what the difference between the edit() and openPersistentEditor() is?

    I've read the docs and googled a bit, but can't find anything relevant. The only thing that docs point out clearly is that edit() doesn't update currentIndex(), so you have to do it before by hand if you plan on using the keyboard and not going crazy on the attempt.

    Once again, thank you so much :)



  • you welcome! :)

    I checked a little bit the qt's source and both edit() and openPersistentEditor() look quite similar so that the problem is elsewhere ... I think that doing other model updates in onDataChanged determines other dataChange signal emitting ... perhaps there you should have avoided that ... instead in closeEditor slot you can do other updates



  • That's what I figured. The code performs much much better in this place. When doing this stuff in the main class using onDataChanged() as a trigger performance was horrible, plus the whole thing behaved quite erratically (even when I had been careful enough to block the signals while setting the fields so that the slot wasn't called in a cascade fashion).

    My current implementation of looks like this:

    @void CompletionDelegate::setFieldValues(QWidget *widget)
    {
    Q_UNUSED(widget);

    // "model" is for the autocompleter, it holds all the valid values
    // "tv_model" is the holder of the rows in the current table view, it's a proxy pointing to "general_model"
    // "general_model" is the model holding ALL the budget rows. Several table views are proxy'ed to this one
    
    QModelIndex index;
    index = tv->currentIndex();
    
    mySortFilterProxyModel *tv_model = new mySortFilterProxyModel;
    tv_model = qobject_cast<mySortFilterProxyModel*>(tv->model());
    
    QSqlTableModel *general_model = new QSqlTableModel;
    general_model = qobject_cast<QSqlTableModel*>(tv_model->sourceModel());
    
    QModelIndex source_index;
    source_index = tv_model->mapToSource(index);
    
    /* do all the magic here, read fields, set fields, whatever */
    

    }
    @

    Then you just need to

    @ connect(this, SIGNAL(closeEditor(QWidget*)), this, SLOT(setFieldValues(QWidget*)));@

    And that's about it. For future reference, and in case someone else finds this useful.



  • This is mostly solved, but there's one thing still... I don't want to open a new thread (not for now at least) because it's so tightly connected to this.

    This setFieldValues() function above is connected to closeEditor(), right? I am now reimplementing eventfilter() to get a finer control on what the delegate does when some keys are pressed. So far, I've managed to make it revert the changes in the current row when pressing ESC. But I am not sure how would I go about editing the next cell or adding a new row "the right way (tm)".

    The sure thing is that, once the description field is edited, setFieldValues() must be called so the rest of the row makes sense, before skipping to the next field.

    So, I guess the sanest approach would be to emit closeEditor() from eventFilter(), that should trigger setFieldValues() conveniently.

    But, for that, I have to have a QWidget*. Since my func is not using that for anything, I've tried to just call it with a new one, but the thing is not working as it should, and by debugging the eventFilter() (I'm discovering how painful that is, by the way) it seems that the indexes are invalid.

    Right now I have this:

    @
    bool CompletionDelegate::eventFilter(QObject *object, QEvent *event)
    {
    QModelIndex index;
    index = tv->currentIndex();

    mySortFilterProxyModel *tv_model = new mySortFilterProxyModel;
    tv_model = qobject_cast<mySortFilterProxyModel*>(tv->model());
    
    QSqlTableModel *general_model = new QSqlTableModel;
    general_model = qobject_cast<QSqlTableModel*>(tv_model->sourceModel());
    
    QModelIndex source_index;
    source_index = tv_model->mapToSource(index);
    
    if(event->type() == QEvent::KeyPress)
    {
        QKeyEvent *key_event = static_cast<QKeyEvent*>(event);
        if(key_event->key() == Qt::Key_Escape)
        {
            general_model->revertAll();
            return true;
        }
        else if(/*key_event->key() == Qt::Key_Return ||*/
                key_event->key() == Qt::Key_Tab)
        {
            if(index.column() == 3)
            {
                emit closeEditor(new QWidget);
                return true;
            }
        }
    }
    return QStyledItemDelegate::eventFilter(object, event);
    

    }
    @

    There must be something fundamentally wrong with my approach, but I am new to this stuff and I am not sure what kind of functionality is supposed to go in each place...

    Can someone give me some tip on how to proceed?

    Thank you :)



  • I think you should let QStyledItemDelegate::eventFilter() with its default implementation for Tab,Backtab,Enter,Return,Esc ... and install an event filter for table view object and treat there mostly Tab and Backtab: on Tab do your row insertion and for both return true, for filtering the event out to prevent the editor opening (default implementation for delegate eventFilter) ... just for the record: QStyledItemDelegate::eventFilter() does filtering for its own editors while an intalled filter does this for other one(who installed the filter) ... so you can insatll even the delegate object as event filter for table view, but then it must be checked to which object was sent the Tab key ... perhaps is better to use other QObject class to do filtering

    hope this helps you :)

    Cheers!



  • Thanks. With all your suggestions I have this working the way I wanted now. There's still place for some refinement here and there, but I'll worry about that at a later stage :)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.