代码之家  ›  专栏  ›  技术社区  ›  Deep

Qt,运行缓慢时不要冻结GUI输入元素

  •  1
  • Deep  · 技术社区  · 6 年前

    当搜索输入值改变时,调用槽:

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

    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:是的,我尝试为slot设置Qt::QueuedConnection,但没有帮助。

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

    2 回复  |  直到 6 年前
        1
  •  1
  •   Kuba hasn't forgotten Monica    6 年前

    不能仅“解冻”输入小部件。输入被冻结是因为您很长一段时间没有将控件返回到事件循环,因此整个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;
      });
    }
    

    前面两种方法在代码外观上相当相似,并且是以连续传递的方式编写的。当然,如果有联谊会就好得多了——这是一件必须做的事。

    以最通用的方式协同运行代码可以执行以下操作:

    /// 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++有短路评估,而三元运算体操是不必要的。

        2
  •  0
  •   jwernerny    6 年前

    另一种方法是在执行长处理操作时手动调用事件循环。这可以通过使用 QCoreApplication::processEvents(..) 在你的搜索循环中,但只是每隔一段时间,否则你会浪费更多的时钟周期。

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