3
votes

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

1
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
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
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
Could you publish your code to github or somewhere else?paceholder
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

1 Answers

2
votes

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.