Qt,操作缓慢时不要冻结GUI输入元素

时间:2018-08-20 10:14:00

标签: c++ qt

我将QLineEdit作为搜索输入。

当搜索输入的值更改时,它会调用广告位:

void
myWidget::slotApplyItemsFilter(const QString &searchString)
{
    viewArea.applyItemsFilter(searchString.trimmed());
}

这是applyItemsFilter方法的实现:

void
myViewArea::applyItemsFilter(const QString &searchString)
{
    for (int i = 0; i < model.rowCount(QModelIndex()); i += 1) {
        setRowHidden(
            i,
            searchString.isEmpty()
                ? false
                : !model.isMatched(i, searchString)
        );
    }
}

在这里实现模型的isMatched方法:

bool
myModel::isMatched(
    const int      row,
    const QString &searchString
) const
{
    return (
        (0 > row && items.size() <= row)
            ? false
            : items.at(row).name.contains(searchString, Qt::CaseInsensitive)
    );
}

一切正常。但是,当视图/模型包含许多项目(例如1000)时,它会缓慢运行并冻结QLineEdit(无法键入符号,不,我可以,但是它看起来像冻结的队列),而不会为每个项目计算。

UPD:是的,我尝试为插槽设置Qt :: QueuedConnection,这无济于事。

如何进行不冻结的搜索输入?

2 个答案:

答案 0 :(得分:1)

您不能仅“解冻”输入窗口小部件。输入被冻结是因为您很长时间没有将控制权返回到事件循环,因此整个GUI被冻结了。

排队的连接无济于事,因为您要批量运行所有代码。

一项基本改进是在更改行可见性​​时禁用视图小部件更新。

最简单的方法是使用过滤代理模型。

class myViewArea : .... {
  QSortFilterProxyModel viewModel;
  ...
};

myViewArea::myViewArea(...) {
  ...
  viewModel.setSourceModel(&model);
  viewModel.setFilterKeyColumn(...); // the column holding the name
  viewModel.setFilterCaseSensitivity(false);
  setModel(&viewModel);
}

void myViewArea::applyItemsFilter_viewModel(const QString &needle) {
  // allows all when needle is empty
  NoUpdates noUpdates(this);
  viewModel.setFilterFixedString(needle);
}

class NoUpdates {
  Q_DISABLE_COPY(NoUpdates)
  QWidget *const w;
  bool const prev = w->updatesEnabled();
public:
  NoUpdates(QWidget *w) : w(w) { w->setUpdatesEnabled(false); }
  ~NoUpdates() { w->setUpdatesEnabled(prev); }
};

或者,您需要使代码不会长时间阻塞事件循环。一种方法是与主线程协同运行搜索。最好在最小化重绘成本的方向上迭代索引,即始终从模型的部分开始,该部分由工作最多的行所描绘,具有最多的项目。

void myViewArea::applyItemsFilter_gui(const QString &needle)
{
  bool inFirstHalf = rowAt(0) <= model.rowCount()/2;
  // iterate backwards in the first half of the rows
  int dir = inFirstHalf ? -1 : +1;
  int i = dir > 0 ? model.rowCount()-1 : 0;
  auto isValidRow = [this, dir](int i){
    return (dir > 0 && i < model.rowCount()) || i >= 0;
  };

  runCooperatively(this, [this, needle, isValidRow, i, dir, 
                          n = NoUpdates(this)]() mutable
  {
    if isValidRow (i) do {
      setRowVisible(i, needle.isEmpty() || model.isMatched(i, needle), this);
      i += dir;
    } while isRowInViewport(i); // update all visible rows in one go
    return isValidRow(i);
  });
}

bool myViewArea::isRowInViewport(int row) const {
  auto first = indexAt(viewport()->rect().topLeft());
  auto last = indexAt(viewport()->rect().bottomRight());
  return row >= 0 && row < model.rowCount() 
         && row >= first.row() && (!last.isValid() || row <= last.row());
}

另一种方法是同时运行不需要在主线程上的代码。名称收集在一个列表对象中,并传递给计算可见性的并发代码。计算可见性后,将在主线程中协同设置行可见性。

void myViewArea::applyItemsFilter_concurrent(const QString &needle)
{
  auto visible = QtConcurrent::mapped(getNames(), 
    [needle](const QString &name){ return isMatched(needle, name); });
  runCooperativelyAfter(this, future, [this, visible, i = 0,
    n = NoUpdates(this)]() mutable
  {
    if (i >= rowCount() || i >= visible.resultCount()) return false;
    setRowVisible(i, visible.resultAt[i], this);
    return ++i;
  });
}

前面两种方法在代码外观上非常相似,并以连续传递样式编写。当然,使用协程会更好-这就是TODO。

可以以最通用的方式协同运行代码,如下所示:

/// Runs a functor cooperatively with the event loop in the context object's
/// thread, as long as the functor returns true. The functor will run at least
/// once, unless the context object gets destroyed before control returns to
/// event loop.
template <class Fun>
static void runCooperatively(QObject *context, Fun &&fun) {
  QMetaObject::invokeMethod(context, [context, f = std::forward<Fun>(fun)]{
    auto *hook = new QTimer(context);
    QObject::connect(hook, &QTimer::timeout, [hook, fun = std::forward<Fun>(f)]{
      if (!fun()) hook->deleteLater();
    });
    hook->start();
  });
}

/// Runs a functor cooperatively after a future is completed
template <class Fun, typename Res>
static void runCooperativelyAfter(QObject *ctx, const QFuture<Res> &future, Fun &&fun) {
  auto *watcher = new QFutureWatcher<Res>(ctx);
  watcher->setFuture(future);
  QObject::connect(watcher, &QFutureWatcher::finished, 
    [future, ctx, f = std::forward<Fun>(fun) {
      future->deleteLater();
      runCooperatively(ctx, [fun = std::forward<Fun>(f)]{ fun(); });
    }
  );
}

其他共享功能如下:

// Note: an empty needle would match per QString search semantics,
// but it's unexpected - better to make it explicit so that
// a maintainer doesn't have to dig in the documentation.
// ***Static Method***
bool myModel::isMatched(const QString &needle, const QString &name)
{
  assert(!needle.isEmpty());
  return name.contains(needle, Qt::CaseInsensitive);
}

bool myModel::isMatched(const int row, const QString &needle) const
{
  return row >= 0 && row < items.size() &&
         isMatched(needle, items.at(row).name);
}

QStringList myModel::getNames() const
{
  // The below should be fast enough, but let's time it to make sure
  return time([this]{
    QStringList names;
    names.reserve(items.size());
    for (auto &item : items) names.push_back(item.name);
    return names;
  });
}

template <class C> static void setRowVisible(int row, bool vis, C *obj) {
  obj->setRowHidden(row, !vis);
}

template <class Fun> 
static typename std::result_of<Fun()>::type time(const Fun &code) {
  struct Timing {
    QElapsedTimer timer;
    Timing () { timer.start(); }
    ~Timing () { qDebug() << timer.elapsed(); }
  } timing;
  return code();
}

您的原始代码具有很多双重反转的逻辑,这使得人们很难理解正在发生的事情。是的,三元运算符的发现可以使我感到头晕,我明白:)然而,这种复杂性是无缘无故的,因为C ++具有短路评估功能,并且三元运算符的体操是不必要的。

答案 1 :(得分:0)

另一种方法是在进行长时间的处理操作时手动调用事件循环。这可以通过在搜索循环中使用QCoreApplication::processEvents(..)来实现,但是只能如此频繁地进行,否则会浪费更多的时钟周期。

paintComponent(Graphics g)