4

I've started to extend the qGet DownloadManager to emit the progress of a TransferItem, so that i can connect to it. I'm inserting the progress data into a cell of a TableView model for display with an Delegate, finally the delegate paints the progress bar. That works in theory, but i'm running into the following

Problem: when there are multiple downloads in parallel, then i get progress updates from both signals into both cells!

enter image description here

Both progress bars show progress data, but the signal is kind of mixed and not unique to the current index (QModelIndex index / index.row()).

(Please ignore the small transitioning problem between UserRoles (after clicking the download button "ActionCell" is displayed and then "Install", before the "ProgressBar" shows up.). That is not the main problem here. My question is about the index problem.) The text "112" and "113" is the int index.row.

Questions:

  • How to update a TableView with progress data for multiple ProgressBars?
  • What must i change to render a progress bar for each download?

Source

Emit progress of a download

I've added the following things to re-emit the signal through the classes, until it bubbles up to the top, where it becomes connectable from the GUI.

  1. a connection from QNetworkReply - downloadProgress(qint64,qint64) to TransferItem - updateDownloadProgress(qint64,qint64)

    void TransferItem::startRequest()
    {       
        reply = nam.get(request);
    
        connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
        connect(reply, SIGNAL(downloadProgress(qint64,qint64)), 
                this, SLOT(updateDownloadProgress(qint64,qint64)));
        connect(reply, SIGNAL(finished()), this, SLOT(finished()));
    
        timer.start();
    }
    
  2. the SLOT function TransferItem - updateDownloadProgress(qint64,qint64) as receiver calculates the progress and stores it in progress (QMap<QString, QVariant>). After the calculation the downloadProgress(this) signal is emitted.

    // SLOT
    void TransferItem::updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
    {
        progress["bytesReceived"] = QString::number(bytesReceived);
        progress["bytesTotal"]    = QString::number(bytesTotal);
        progress["size"]          = getSizeHumanReadable(outputFile->size());
        progress["speed"]         = QString::number((double)outputFile->size()/timer.elapsed(),'f',0).append(" KB/s");
        progress["time"]          = QString::number((double)timer.elapsed()/1000,'f',2).append("s");
        progress["percentage"]    = (bytesTotal > 0) ? QString::number(bytesReceived*100/bytesTotal).append("%") : "0 %";
    
        emit downloadProgress(this);
    }
    
    QString TransferItem::getSizeHumanReadable(qint64 bytes)
    {
        float num = bytes; QStringList list;
        list << "KB" << "MB" << "GB" << "TB";    
        QStringListIterator i(list); QString unit("bytes");    
        while(num >= 1024.0 && i.hasNext()) {
         unit = i.next(); num /= 1024.0;
        }
        return QString::fromLatin1("%1 %2").arg(num, 3, 'f', 1).arg(unit);
    }
    
  3. When a new download is enqueued, i'm connecting the emitted downloadProgress(this) to the Slot DownloadManager - downloadProgress(TransferItem*). (dl is DownloadItem which extends TransferItem).

    void DownloadManager::get(const QNetworkRequest &request)
    {
        DownloadItem *dl = new DownloadItem(request, nam);
        transfers.append(dl);
        FilesToDownloadCounter = transfers.count();
    
        connect(dl, SIGNAL(downloadProgress(TransferItem*)),
                SLOT(downloadProgress(TransferItem*)));
        connect(dl, SIGNAL(downloadFinished(TransferItem*)),
                SLOT(downloadFinished(TransferItem*)));
    }
    
  4. Finally, i'm re-emitting the download progress one more time:

    void DownloadManager::downloadProgress(TransferItem *item)
    {
        emit signalProgress(item->progress);
    }
    

Now the TableView with Delegate, doDownload(index) and ProgressBarUpdater

  1. QTableView
  2. with added QSortFilterProxyModel (for case-insensitivity)
  3. with added ColumnDelegate, which renders DownloadButton and ProgressBar based on custom UserRoles. The delegate handles the button click: the SIGNAL downloadButtonClicked(index) is emited from the editorEvent(event, model, option, index) method.

    actionDelegate = new Updater::ActionColumnItemDelegate;
    ui->tableView->setItemDelegateForColumn(Columns::Action, actionDelegate);
    
    connect(actionDelegate, SIGNAL(downloadButtonClicked(QModelIndex)), this, SLOT(doDownload(QModelIndex)));
    
  4. The doDownload method receives the index and fetches the download URL from the model. Then the URL is added to the DownloadManager and i'm setting up a ProgressBarUpdater object to set the progress data to the model at the given index. Finally i'm, connecting downloadManager::signalProgress to progressBar::updateProgress and invoke the downloadManager::checkForAllDone to start the download processing.

    void UpdaterDialog::doDownload(const QModelIndex &index)
    {        
        QUrl downloadURL = getDownloadUrl(index);
        if (!validateURL(downloadURL)) return;
    
        QNetworkRequest request(downloadURL);           
        downloadManager.get(request); // QueueMode is Parallel by default
    
        ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
        progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
    
        connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
                progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
    
        QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
    }
    
  5. The model update part: the ProgressBarUpdater takes the index and the progress and should update the model at the given index.

    ProgressBarUpdater::ProgressBarUpdater(UpdaterDialog *parent, int currentIndexRow) :
        QObject(parent), currentIndexRow(currentIndexRow)
    {
        model = parent->ui->tableView_1->model();
    }
    
    void ProgressBarUpdater::updateProgress(QMap<QString, QVariant> progress)
    {
        QModelIndex actionIndex = model->index(currentIndexRow, UpdaterDialog::Columns::Action);
    
        // set progress to model
        model->setData(actionIndex, progress, ActionColumnItemDelegate::DownloadProgressBarRole);
    
        model->dataChanged(actionIndex, actionIndex);
    }
    
  6. The rendering part: i'm rendering the fake ProgressBar from the delegate; fetching the progress data with index.model()->data(index, DownloadProgressBarRole).

    void ActionColumnItemDelegate::drawDownloadProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QStyleOptionProgressBarV2 opt;
        opt.initFrom(bar);
        opt.rect = option.rect;
        opt.rect.adjust(3,3,-3,-3);
        opt.textVisible = true;
        opt.textAlignment = Qt::AlignCenter;
        opt.state = QStyle::State_Enabled | QStyle::State_Active;
    
        // get progress from model
        QMap<QString, QVariant> progress = 
            index.model()->data(index, DownloadProgressBarRole).toMap();
    
        QString text = QString::fromLatin1(" %1 %2 %3 %4 %5 ")
            .arg(QString::number(index.row()))
            .arg(progress["percentage"].toString())
            .arg(progress["size"].toString())
            .arg(progress["speed"].toString())
            .arg(progress["time"].toString());
    
        opt.minimum  = 0;
        opt.maximum  = progress["bytesTotal"].toFloat();
        opt.progress = progress["bytesReceived"].toFloat();
        opt.text     = text;
    
        bar->style()->drawControl(QStyle::CE_ProgressBar,&opt,painter,bar);
    }
    

I've added QString::number(index.row() to the progress bar text, so that each ProgressBar gets its row number rendered. In other words: the rendering is unique to the row, but the incoming progress data is somehow mixed.

I'm stuck on the index problem for a while now. Thank you in advance for your help.

Update: The issue is resolved!

Thank you very much ddriver!! I followed your suggestions and fixed it:

enter image description here

Jens A. Koch
  • 39,862
  • 13
  • 113
  • 141
  • Your implementation is needlessly over-complicated, no wonder things are getting messed up. Have you tried actually debugging, or at least putting a few `qDebug()`s to pinpoint what and where goes wrong? – dtech Apr 21 '16 at 17:53
  • I've removed the debug lines intentionally. "Your implementation is needlessly over-complicated". Would you be so nice to explain, how to simplify it? – Jens A. Koch Apr 21 '16 at 20:34
  • 2
    I mostly agree with ddriver. It's impossible to get what's going wrong by looking at this big incomplete piece of code. You should provide a minimal complete example or just simplify it. Remove the delegate, set item text directly from the updater object, add some debugging to see if correct items are updated and so on... – hank Apr 22 '16 at 07:53
  • Could you publish your code to github or somewhere else? – paceholder Apr 22 '16 at 09:35
  • IMO the whole problem is cursed by wrong design. I don't see anything about data model you are using. Is it a `QStandardItemModel` or did you sub classed `QAbstractTableModel`? Progress information should update the data model nothing else. `QTableView` should react on changes in data model. If this part is done properly than `QSortFilterProxyModel` should work out of the box. – Marek R Apr 22 '16 at 10:38
  • Its a `QStandardItemModel`. I've used `setData()` to set different progress percentages on various rows to implement the `paint()` method of the delegate. Its not a problem of setting or getting data from/to the model. The progress information updates the model. The delegate renders the information of the model at the correct index. The `QTableView` reacts on changes.--- I'm still not sure, what you mean by wrong design. Using `QStandardItemModel` for a `QTableView` with added `QSortFilterProxyModel` is common. So that part is not a wrong design by itself. – Jens A. Koch Apr 22 '16 at 11:43
  • The only part where i have a bad feeling about the design, is the usage of a delegate to render fake widgets. But thats often suggested and accepted on SO and the Qt forums. Its probably a downside when working with a TableView instead of TableWidget. I've made the design decision to use a TableView intentionally, because i think that it makes working with model and sorting a bit easier, when compared to a TableWidget; but with the tradeoff, that you have less cell control and no option to set widgets directly (QTableWidget::setCellWidget). Thats solved. – Jens A. Koch Apr 22 '16 at 11:43
  • @JensA.Koch The QtWidgets stack is a terrible drag to work with, and its model/view/delegate is probably the ugliest part of it. You can do the UI in QML it like 1/10 of the time. Also I don't see you reacting to the answer, does it not solve it for you? – dtech Apr 22 '16 at 12:17

1 Answers1

2

The DownloadManager tracks the progress for all transfers, and you keep each transfer item's data in the respective TransferItem.

The logical thing IMO would be to have a connection from each TransferItem to the corresponding ProgressBarUpdater, and emit from the transfer item.

However, in your case, you are reporting progress not from each individual transfer item, but from the download manager. So each time you are emitting a progress, you are emitting the progress for a particular transfer item to all progress bars.

connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
            progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));

So instead of a

TransferItem --progress--> CorrespondingUI

you have a:

TransferItem --transferItem--> DownloadManager --progress--> AllUIs

This leads to having one single and varying progress for all progress bars, which corresponds to the last download that happen to report progress before the UI updated. Which is why you get no more variation after the first download is completed, as the manager only updates the progress for the second.

Finally, i'm re-emitting the download progress one more time:

void DownloadManager::downloadProgress(TransferItem *item)
{
    emit signalProgress(item->progress);
}

And who exactly needs an anonymous progress, containing no information whatsoever to which transfer it applies? Aside from the bug of course.

Would you be so nice to explain, how to simplify it?

I was at the end of my mental rope yesterday when I commented, on a clear head it doesn't look all that overdone, but still I'd probably go for something more streamlined, involving 3 key components only:

DownloadsManager -> DownloadController -> UI
                 -> DownloadController -> UI

It just seems redundant to have a DownloadItem and then also a TransferItem, considering that a download is a transfer.

The model and view are totally unnecessary as well, as is storing the progress in the model rather than just having it as a member of the progress bar. You could go with just a regular widget for each download, and place them in a vertical layout.

Update:

The excessive, unnecessary compartmentalization has led to a level of fragmentation which makes it hard to get to the data, needed to make it work once you put everything together. The main issue is you have no way to tie a transfer item to the right progress bar updater, and since you still haven't posted all of the relevant code, the simplest possible solution I can offer involves the following minor changes:

// in DownloadManager
void signalProgress(QMap<QString, QVariant>); // this signal is unnecessary, remove 
void DownloadManager::downloadProgress(TransferItem *item) // change this
{
    registry[item->request.url()]->updateProgress(item->progress);
}
QMap<QUrl, ProgressBarUpdater *> registry; // add this

// in UpdaterDialog
void UpdaterDialog::doDownload(const QModelIndex &index)
{        
    QUrl downloadURL = getDownloadUrl(index);
    if (!validateURL(downloadURL)) return;

    QNetworkRequest request(downloadURL);           
    downloadManager.get(request); // QueueMode is Parallel by default

    ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
    progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );

    // remove the connection - source of the bug, instead register the updater
    downloadManager.registry[downloadURL] = progressBar;

    QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}

That's pretty much it, the progress updater is associated with the URL, and in DownloadManager::downloadProgress instead of emitting the progress to all progress updaters, you simply lookup the one which actually corresponds to the particular download, and only update its progress. It is somewhat clumsy, but as I said, if your design is proper, it would not be needed and you wouldn't have the problem in the first place.

There are other solutions as well:

  • change the DownloadManager's signal to void signalProgress(TransferItem *), and the body of downloadProgress toemit signalProgress(item); , change to void ProgressBarUpdater::updateProgress(TransferItem *), and in the body compare the transfer item's request's url to the one in the model at the currentIndexRow, and only model-setData() if it is the same. This solution is not very efficient, since it will emit to all progress updaters just to modify one.

  • cut out the middleman, what I've been suggesting from the start, make DownloadManager ::get() return a pointer to the DownloadItem/TransferItem created in its body, then in UpdaterDialog::doDownload() you can connect the transfer item directly to the appropriate progress updater, so you will no longer need DownloadManager::downloadProgress() and the signalProgress signal, you only need to change the signature of the signal in TransferItem to void downloadProgress(QMap<QString, QVariant>); and emit the progress rather than the item. This is actually the most efficient solution, as it involves nothing extra, jsut the removal of unnecessary stuff.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • Sorry for the delay. Yes, thats spot on - the problem is the connection from downloadManager to progressBar. But i'm still not sure how to implement it. -- "anonymous progress, containing no information whatsoever to which transfer it applies" Should i pass the index id into the downloadManager to set it to the TransferItem and let it bubble up together the progress data? Or emit the TransferItem itself? If i don't pass the id, i could use the URL to relate it back to the row. Hmm... – Jens A. Koch Apr 22 '16 at 12:53
  • In regard to the redundancy of `DownloadItem`+`TransferItem`: Yes, its possible to get rid of the `TransferItem` and work with a `DownloadItem` object only. Its Qt code: https://github.com/qtproject/qtbase/blob/dev/tests/manual/qnetworkaccessmanager/qget/transferitem.cpp I guess they used this approach to differentiate between Upload and Download object based on the request method. – Jens A. Koch Apr 22 '16 at 12:58
  • You don't need a connection between the manager and the progress bar, the progress bar is per item, the manager is not, but generally, you don't want "one to many" but "one to one connection" - sure you could pass along an index, and have the signal ignored in all but the proper index, but that's needless complexity and overhead. – dtech Apr 22 '16 at 12:58
  • The Qt people - they love bloat and mess, maybe they are paid on LOC basis, maybe they like their code hard to maintain to make themselves indispensable, I do not know, but this reflects on the private APIs and above-trivial examples they write as well, in the case of examples producing complex to learn from and clumsy to extend and build on code. – dtech Apr 22 '16 at 13:00
  • @JensA.Koch - I think I may have found a solution – dtech Apr 22 '16 at 16:50
  • Yes, you did! And you were right from the start: the problem was the connection to the downloadManager, which should have been a connection to an individual transfer. I followed your suggestion and was able to fix it. Screenshot attached as update to my question. --- I really appreciate your help! Thank you very much for taking the time to think about the problem and for writing a detailed answer! -- Bounty time :) – Jens A. Koch Apr 23 '16 at 18:56
  • 1
    @JensA.Koch - you are welcome. The issue was actually rather simple, only not immediately clear due to the tangled design, IMO it didn't really merit such a generous bounty, but I know the feeling of being stuck. The real lesson here is: start with the bare minimum and only expand whenever necessary, a smaller design is much more clear conceptually, and as you gradually expand it you get to know it better, in contrast a design over-complicated from the get go is complex to visualize and put together properly. – dtech Apr 23 '16 at 19:09