如何使用多个 ProgressBar 的进度数据更新 TableView?

How to update a TableView with progress data for multiple ProgressBars?

我已经开始扩展 qGet DownloadManager 以发出 TransferItem 的进度,以便我可以连接到它。我将进度数据插入到 TableView 模型的单元格中,以便使用 Delegate 显示,最后委托绘制进度条。这在理论上可行,但我 运行 进入以下

问题: 当有多个并行下载时,我从 both signals 获得进度更新到 both细胞!

两个进度条都显示进度数据,但信号是混合的并且不是当前索引所独有的 (QModelIndex index / index.row())。

(请忽略 UserRoles 之间的小转换问题(点击下载按钮后显示 "ActionCell",然后显示 "Install",然后才显示 "ProgressBar"。)。那不是这里的主要问题。我的问题是关于索引问题。)文本“112”和“113”是int index.row.

问题:


来源

发出下载进度

我添加了以下内容以通过 类 重新发射信号,直到它冒泡到顶部,在那里它可以从 GUI 连接。

  1. QNetworkReply - downloadProgress(qint64,qint64)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. SLOT函数TransferItem - updateDownloadProgress(qint64,qint64)作为接收方计算进度并将其存储在progressQMap<QString, QVariant>)中。 计算后发出 downloadProgress(this) 信号。

    // 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. 当新的下载排队时,我将发出的 downloadProgress(this) 连接到插槽 DownloadManager - downloadProgress(TransferItem*)。 (dlDownloadItem 扩展 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. 最后再发一次下载进度:

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

现在是带有 Delegate、doDownload(index) 和 ProgressBarUpdater 的 TableView

  1. QTableView
  2. 添加了 QSortFilterProxyModel(不区分大小写)
  3. 添加了 ColumnDelegate,它根据自定义 UserRoles 呈现 DownloadButton 和 ProgressBar。委托处理按钮单击:SIGNAL downloadButtonClicked(index) 是从 editorEvent(event, model, option, index) 方法发出的。

    actionDelegate = new Updater::ActionColumnItemDelegate;
    ui->tableView->setItemDelegateForColumn(Columns::Action, actionDelegate);
    
    connect(actionDelegate, SIGNAL(downloadButtonClicked(QModelIndex)), this, SLOT(doDownload(QModelIndex)));
    
  4. doDownload 方法接收 index 并从模型中获取下载 URL。然后将 URL 添加到 DownloadManager 我正在设置一个 ProgressBarUpdater 对象以将进度数据设置为给定索引处的模型。最后,我将 downloadManager::signalProgress 连接到 progressBar::updateProgress 并调用 downloadManager::checkForAllDone 开始下载处理。

    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. 模型更新部分:ProgressBarUpdater 获取索引和进度,并应在给定索引处更新模型。

    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. 渲染部分:我正在渲染委托中的假 ProgressBar;使用 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);
    }
    

我已将 QString::number(index.row() 添加到进度条文本中,以便每个 ProgressBar 都能呈现其行号。换句话说:渲染对于行是唯一的,但是传入的进度数据不知何故是混合的。

我在索引问题上卡住了一段时间。预先感谢您的帮助。

更新:问题已解决!

非常感谢ddriver!!我听从了你的建议并修复了它:

DownloadManager 跟踪所有传输的进度,并且您将每个传输项目的数据保存在各自的 TransferItem

IMO 的合乎逻辑的事情是从每个 TransferItem 到相应的 ProgressBarUpdater 建立连接,并从传输项目中发出。

但是,在您的情况下,您报告的进度不是来自每个单独的传输项目,而是来自下载管理器。因此,每次发出进度时,您都会将特定传输项目的进度发出到 all 个进度条。

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

所以不是

TransferItem --progress--> CorrespondingUI

你有一个:

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

这导致所有进度条都有一个单一且不同的进度,这对应于 UI 更新之前碰巧报告进度的最后一次下载。这就是为什么在第一次下载完成后您不会再得到变化,因为管理器只更新第二次的进度。

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

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

谁真正需要匿名进程,不包含任何信息适用于哪个传输?当然除了bug。

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

昨天我发表评论时,我已经筋疲力尽了,在头脑清醒的情况下,它看起来并不过分,但我仍然可能会选择更精简的东西,只涉及 3 个关键部分:

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

DownloadItemTransferItem 似乎是多余的,考虑到下载就是传输。

模型和视图也完全没有必要,因为将进度存储在模型中而不是仅仅将其作为进度条的成员。您可以为每次下载只使用一个常规小部件,并将它们放置在垂直布局中。

更新:

过度的、不必要的划分导致了一定程度的碎片化,这使得很难获得数据,一旦你把所有东西放在一起就需要让它发挥作用。主要问题是您无法将传输项目绑定到正确的进度条更新程序,并且由于您仍然 尚未发布所有相关代码,所以我可以采用最简单的解决方案报价涉及以下细微变化:

// 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);
}

差不多就是这样,进度更新器与 URL 相关联,并且在 DownloadManager::downloadProgress 中不会将进度发送给 所有 进度更新器,您只需查找实际对应于特定下载的那个,并且只更新其进度。它有点笨拙,但正如我所说,如果你的设计是正确的,就不需要它,你一开始就不会有问题。

还有其他解决方案:

  • 把DownloadManager的signal改成void signalProgress(TransferItem *)downloadProgress的body改成emit signalProgress(item);,改成void ProgressBarUpdater::updateProgress(TransferItem *),body比较将项目的请求 url 转移到 currentIndexRow 模型中的请求,如果相同则仅 model-setData() 。这个解决方案不是很有效,因为它会发送给所有进度更新器,只是为了修改一个。

  • 去掉中间人,我从一开始就建议,让 DownloadManager ::get() return 指向 DownloadItem/TransferItem 在其主体中创建,然后在 UpdaterDialog::doDownload() 中您可以将传输项目直接连接到适当的进度更新器,因此您将不再需要 DownloadManager::downloadProgress()signalProgress 信号,您只需要将 TransferItem 中的信号签名更改为 void downloadProgress(QMap<QString, QVariant>); 并发出进度而不是项目。这实际上是最有效的解决方案,因为它不涉及任何额外的东西,只是删除了不必要的东西。