实现 QTableView 的部分移动正确。

huangapple go评论74阅读模式
英文:

Implementing QTableView sections move properly

问题

Here is the translated content:

当我调用以下代码:

tableView->horizontalHeader()->setSectionsMovable(true);

拖动一个部分轻微向右并释放鼠标按钮会将表格滚动到最左边(参见 gif
是否有解决此问题的方法?Qt 版本为 6.5.0。

以下是代码:

// main.cpp
#include <iostream>
#include <QApplication>
#include <QTableView>
#include <QHeaderView>
#include "MyModel.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    auto tableView = new QTableView();
    auto model = new MyModel();
    tableView->setModel(model);
    tableView->horizontalHeader()->setSectionsMovable(true);
    tableView->show();
    return app.exec();
}
// MyModel.cpp
#include "MyModel.h"

MyModel::MyModel(QObject *parent)
        : QAbstractTableModel(parent) {
}

int MyModel::rowCount(const QModelIndex &) const {
    return 20;
}

int MyModel::columnCount(const QModelIndex &) const {
    return 20;
}

QVariant MyModel::data(const QModelIndex &index, int role) const {
    if (role == Qt::DisplayRole)
        return QString("%1, %2")
                .arg(index.row() + 1)
                .arg(index.column() + 1);

    return {};
}
// MyModel.h
#ifndef TEST_MYMODEL_H
#define TEST_MYMODEL_H

#include <QAbstractTableModel>

class MyModel : public QAbstractTableModel {
Q_OBJECT

public:
    explicit MyModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;

    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};

#endif //TEST_MYMODEL_H
英文:

When I call

tableView-&gt;horizontalHeader()-&gt;setSectionsMovable(true);

Dragging a section slightly to the right and releasing the mouse button scrolls the table all the way to the left (see
gif)
Are there any ways to solve this? Qt Version is 6.5.0.

The code is below

// main.cpp
#include &lt;iostream&gt;
#include &lt;QApplication&gt;
#include &lt;QTableView&gt;
#include &lt;QHeaderView&gt;
#include &quot;MyModel.h&quot;

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    auto tableView = new QTableView();
    auto model = new MyModel();
    tableView-&gt;setModel(model);
    tableView-&gt;horizontalHeader()-&gt;setSectionsMovable(true);
    tableView-&gt;show();
    return app.exec();
}
// MyModel.cpp
#include &quot;MyModel.h&quot;

MyModel::MyModel(QObject *parent)
        : QAbstractTableModel(parent) {
}

int MyModel::rowCount(const QModelIndex &amp;) const {
    return 20;
}

int MyModel::columnCount(const QModelIndex &amp;) const {
    return 20;
}

QVariant MyModel::data(const QModelIndex &amp;index, int role) const {
    if (role == Qt::DisplayRole)
        return QString(&quot;%1, %2&quot;)
                .arg(index.row() + 1)
                .arg(index.column() + 1);

    return {};
}
// MyModel.h
#ifndef TEST_MYMODEL_H
#define TEST_MYMODEL_H


#include &lt;QAbstractTableModel&gt;

class MyModel : public QAbstractTableModel {
Q_OBJECT

public:
    explicit MyModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &amp;parent = QModelIndex()) const override;

    int columnCount(const QModelIndex &amp;parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &amp;index, int role = Qt::DisplayRole) const override;
};


#endif //TEST_MYMODEL_H

答案1

得分: 1

这很可能是与此处提到的相同的错误:Qt stylesheet conflict between QTableView::item and cellWidget。它是由自动滚动引起的,但在你的情况下是标题视图的自动滚动。

解决方法1:

永久禁用标题视图的自动滚动:

tableView->horizontalHeader()->setAutoScroll(false);

解决方法2:

仅在鼠标光标进入标题视图时禁用自动滚动,并在离开时重新启用,使用一个事件过滤器

#include <QObject>
#include <QEvent>
#include <QHeaderView>

class myEventFilter : public QObject
{
    Q_OBJECT

public:
    myEventFilter(QObject *parent = nullptr) {}

protected:
    bool eventFilter(QObject *obj, QEvent *event) override
    {
        if (event->type() == QEvent::Enter)
        {
            QHeaderView *h = static_cast<QHeaderView*>(obj);

            h->setAutoScroll(false);
        }
        if (event->type() == QEvent::Leave)
        {
            QHeaderView *h = static_cast<QHeaderView*>(obj);

            h->setAutoScroll(true);
        }

        return QObject::eventFilter(obj, event);
    }
};

并像这样在标题视图上安装它:

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->installEventFilter(filter);

解决方法3:

由于前两种解决方法似乎会在将项目移动到视口边缘时禁用所需的自动滚动,这是一种在启用自动滚动的同时消除不需要的自动滚动的解决方法。

它通过仅在光标离开视口时启用自动滚动,否则禁用它来实现。

class myEventFilter : public QObject
{
    Q_OBJECT

public:
    myEventFilter(QObject *parent = nullptr) {}

protected:
    bool eventFilter(QObject *obj, QEvent *event) override
    {
        if (event->type() == QEvent::MouseMove)
        {
            QWidget *w = static_cast<QWidget*>(obj);
            qDebug() << w->mapFromGlobal(QCursor::pos());
            QTableView *h = static_cast<QTableView*>(obj->parent());

            // 这个条件将全局光标位置转换为视口的本地位置,并检查是否离开了它
            if (w->mapFromGlobal(QCursor::pos()).x() <= 0)
                h->setAutoScroll(true);
            else
                h->setAutoScroll(false);
        }

        return QObject::eventFilter(obj, event);
    }
};

并像这样在标题视图的视口上安装它:

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->viewport()->installEventFilter(filter);

这种解决方法仍然有一个问题,一旦自动滚动开始,它就不会停止,但至少它只会像正常的自动滚动一样触发,但如果你有大量列,这将是一个问题。

解决方法4:

由于QTableView中的自动滚动似乎混乱不堪,我想到了禁用它并重新实现它的方法。

这很简单。如果光标位于边缘,则滚动表视图的水平滚动条。

下面是实现方法:

class myEventFilter : public QObject
{
    Q_OBJECT

public:
    myEventFilter(QObject *parent = nullptr) {}
    bool pressed = false;

protected:
    bool eventFilter(QObject *obj, QEvent *event) override
    {
        if (event->type() == QEvent::MouseButtonPress)
        {
            pressed = true;
        }
        if (event->type() == QEvent::MouseButtonRelease)
        {
            pressed = false;
        }

        QHeaderView *h = static_cast<QHeaderView*>(obj->parent());
        QTableView *t = static_cast<QTableView*>(obj->parent()->parent());

        if (h->mapFromGlobal(QCursor::pos()).x() <= 0 && pressed)
        {
            t->horizontalScrollBar()->setValue(t->horizontalScrollBar()->value() - 1);
        }
        if (h->mapFromGlobal(QCursor::pos()).x() >= t->viewport()->visibleRegion().boundingRect().width() && pressed)
        {
            t->horizontalScrollBar()->setValue(t->horizontalScrollBar()->value() + 1);
        }

        return QObject::eventFilter(obj, event);
    }
};

然后,像这样在标题视图的视口上安装事件过滤器,并禁用自动滚动:

tableView->horizontalHeader()->setAutoScroll(false);

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->viewport()->installEventFilter(filter);

pressed 布尔值是为了在你没有尝试滚动时避免表视口滚动,因此只有在你按住标题视图上的项目时才会滚动。

我还添加了从左向右滚动的功能。

注意:

由于事件过滤器中传递的事件太多,导致滚动速度过快,我想到了一种通过仅在发送自定义事件时滚动来控制滚动速度的方法,可以使用QTimer来控制速度:

下面是实现方法:

class myEventFilter : public QObject
{
    Q_OBJECT

public:
    myEventFilter(QObject *parent = nullptr) {}
    bool pressed = false;
    QTimer *timer = new QTimer();

protected:
    bool eventFilter(QObject *obj, QEvent *event) override
    {
        if (event->type() == QEvent::MouseButtonPress)
        {
            pressed = true;
        }
        if (event->type() == QEvent::MouseButtonRelease)
        {
            pressed = false;
        }
        // 检查是否是我们发送的自定义事件,我使用了 QEvent::None 以避免引发不希望的效果
        if (event->type() == QEvent::None)
        {
            QHeaderView *h = static_cast<QHeaderView*>(obj->parent());
            QTableView *t = static_cast<QTableView*>(

<details>
<summary>英文:</summary>

This is most likely the same bug mentioned in here: [Qt stylesheet conflict between QTableView::item and cellWidget](https://stackoverflow.com/questions/75927032/qt-stylesheet-conflict-between-qtableviewitem-and-cellwidget/75943506#75943506). It&#39;s caused by auto scrolling, but in your case the header view&#39;s auto scrolling.

**Solution 1:**

Disable auto scrolling of your header view permanently with this:

tableView->horizontalHeader()->setAutoScroll(false);

-------------
**Solution 2:**

Disable auto scrolling of your header view only when the cursor enters it, and enable it back again when it leaves it, by using an **event filter**:

#include <QObject>
#include <QEvent>
#include <QHeaderView>

class myEventFilter : public QObject
{
Q_OBJECT

public:
myEventFilter (QObject *parent = nullptr) {}

protected:
bool eventFilter(QObject * obj, QEvent * event) override
{
if(event->type() == QEvent::Enter)
{
QHeaderView h = static_cast<QHeaderView>(obj);

        h-&gt;setAutoScroll(false);
    }
    if(event-&gt;type() == QEvent::Leave)
    {
        QHeaderView *h = static_cast&lt;QHeaderView*&gt;(obj);

        h-&gt;setAutoScroll(true);
    }

    return QObject::eventFilter(obj, event);
}

};

and install it on your header view like this:

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->installEventFilter(filter);

-------------
**Solution 3:**

Since the 2 previous solutions seem to disable a needed auto scroll when moving an item to the viewport&#39;s edge, here&#39;s a workaround that enables that auto scrolling, while getting rid of the unwanted one.

It works by enabling the auto scroll only when the cursor leaves the viewport, and disabling it otherwise.

class myEventFilter : public QObject
{
Q_OBJECT

public:
myEventFilter (QObject *parent = nullptr) {}

protected:
bool eventFilter(QObject * obj, QEvent * event) override
{
if(event->type() == QEvent::MouseMove)
{
QWidget w = static_cast<QWidget>(obj);
qDebug()<<w->mapFromGlobal(QCursor::pos());
QTableView h = static_cast<QTableView>(obj->parent());

        //this condition translates global cursor position to viewport&#39;s local position
        //and checks if it left it
        if(w-&gt;mapFromGlobal(QCursor::pos()).x()&lt;=0)
            h-&gt;setAutoScroll(true);
        else
            h-&gt;setAutoScroll(false);
    }


    return QObject::eventFilter(obj, event);
}

};

and install it on your header view&#39;s viewport like this:

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->viewport()->installEventFilter(filter);

This solution still has one problem, once the auto scroll starts, it does not stop as well, but at least it only gets triggered as normal auto scrolls do, but if you have a large number of columns, it will be a problem.

-------------

**Solution 4:**

Since auto scroll seems to be a mess in `QTableView`, I got the idea to disable it, and reimplement it myself.

It&#39;s very simple. If the cursor is at the edge, scroll the horizontal scroll bar of the table view.

Here&#39;s how:

class myEventFilter : public QObject
{
Q_OBJECT

public:
myEventFilter (QObject *parent = nullptr) {}
bool pressed=false;

protected:
bool eventFilter(QObject * obj, QEvent * event) override
{
if(event->type() == QEvent::MouseButtonPress)
{
pressed = true;
}
if(event->type() == QEvent::MouseButtonRelease)
{
pressed = false;
}

   QHeaderView *h = static_cast&lt;QHeaderView*&gt;(obj-&gt;parent());
   QTableView *t = static_cast&lt;QTableView*&gt;(obj-&gt;parent()-&gt;parent());

   if(h-&gt;mapFromGlobal(QCursor::pos()).x()&lt;=0 &amp;&amp; pressed)
   {
        t-&gt;horizontalScrollBar()-&gt;setValue(t-&gt;horizontalScrollBar()-&gt;value()-1);
   }
   if(h-&gt;mapFromGlobal(QCursor::pos()).x()&gt;=t-&gt;viewport()-&gt;visibleRegion().boundingRect().width() &amp;&amp; pressed)
   {
        t-&gt;horizontalScrollBar()-&gt;setValue(t-&gt;horizontalScrollBar()-&gt;value()+1);
   }

    return QObject::eventFilter(obj, event);
}

};

and again, install the event filter on your header view&#39;s viewport, and disable the auto scrolling.

Here&#39;s how:

tableView->horizontalHeader()->setAutoScroll(false);

myEventFilter *filter = new myEventFilter();
tableView->horizontalHeader()->viewport()->installEventFilter(filter);

The `pressed` boolean is to avoid table viewport from scrolling while you&#39;re not trying to, so it only scrolls if you&#39;re pressing an item on the header view.

I also added the ability to scroll from left to right.

**Note:**

Since the scrolling speed is too fast, due to the huge number of events going through the `eventFilter`, I came up with a way to control scrolling speed, by scrolling only when custom events are sent, at a rate which can be controlled with a `QTimer`:

Here&#39;s how:

class myEventFilter : public QObject
{
Q_OBJECT

public:
myEventFilter (QObject *parent = nullptr) {}
bool pressed= false;
QTimer *timer = new QTimer();

protected:
bool eventFilter(QObject * obj, QEvent * event) override
{
if(event->type() == QEvent::MouseButtonPress)
{
pressed = true;
}
if(event->type() == QEvent::MouseButtonRelease)
{
pressed = false;
}
//check if it's the custom event we sent, I used QEvent::None to avoid causing unwanted effects
if(event->type() == QEvent::None)
{
QHeaderView h = static_cast<QHeaderView>(obj->parent());
QTableView t = static_cast<QTableView>(obj->parent()->parent());

        if(h-&gt;mapFromGlobal(QCursor::pos()).x()&lt;=0 &amp;&amp; pressed)
        {
            t-&gt;horizontalScrollBar()-&gt;setValue(t-&gt;horizontalScrollBar()-&gt;value()-1);
        }
        if(h-&gt;mapFromGlobal(QCursor::pos()).x()&gt;=t-&gt;viewport()-&gt;visibleRegion().boundingRect().width() &amp;&amp; pressed)
        {
            t-&gt;horizontalScrollBar()-&gt;setValue(t-&gt;horizontalScrollBar()-&gt;value()+1);
        }
    }

    return QObject::eventFilter(obj, event);
}

};

and you can use it this way:

auto tableView = new QTableView();
auto model = new MyModel();
tableView->setModel(model);
tableView->horizontalHeader()->setSectionsMovable(true);

tableView->horizontalHeader()->setAutoScroll(false);
myEventFilter *filter = new myEventFilter(tableView);
QObject::connect(filter->timer,&QTimer::timeout,={QApplication::sendEvent(tableView->horizontalHeader()->viewport(),new QEvent(QEvent::None)); });
//change the duration depending on the speed you want
filter->timer->start(40);

tableView->horizontalHeader()->viewport()->installEventFilter(filter);
tableView->show();


-------
Here&#39;s the [bug report](https://bugreports.qt.io/browse/QTBUG-112616), the Qt version mentioned in there is 5.15.2, but it seems to affect newer versions as well, I&#39;m on the same version as you are, Qt 6.5.0.

</details>



# 答案2
**得分**: 0

```c++
在Mihoub Abderrahmene Rayene对答案进行了一些改进后,我提出了以下解决方案:

```c++
// myEventFilter.h

#include <QObject>
#include <QEvent>
#include <QHeaderView>
#include <QTableView>
#include <QScrollBar>
#include <QTimer>

enum MovingState {
    Idle,
    Right,
    Left
};

#define TABLE_SCROLL_OFFSET 0.05

class myEventFilter : public QObject {
Q_OBJECT

public:
    explicit myEventFilter(QTableView *tableView, QObject *parent = nullptr) : tableView(tableView), QObject(parent) {
        scrollTimer.setInterval(10);
        connect(&scrollTimer, &QTimer::timeout, this, &myEventFilter::scrollByInterval);
    }

private:
    bool pressed = false;

    MovingState movingState = Idle;

    QTimer scrollTimer;

    QTableView *tableView;

private slots:

    void scrollByInterval() const {
        int scrollStep = 1;
        QScrollBar *scrollbar = tableView->horizontalScrollBar();

        if (movingState == Right)
            scrollbar->setValue(scrollbar->value() + scrollStep);
        else
            scrollbar->setValue(scrollbar->value() - scrollStep);
    };
protected:
    bool eventFilter(QObject *obj, QEvent *event) override {
        if (event->type() == QEvent::MouseButtonPress) pressed = true;

        if (event->type() == QEvent::MouseButtonRelease) pressed = false;

        if (event->type() == QEvent::MouseMove && pressed) {
            auto *h = dynamic_cast<QHeaderView *>(obj->parent());

            int cursorPosition = h->mapFromGlobal(QCursor::pos()).x();
            int tableWidth = tableView->viewport()->visibleRegion().boundingRect().width();

            if (cursorPosition <= tableWidth * TABLE_SCROLL_OFFSET) movingState = Left;
            else if (cursorPosition >= tableWidth * (1 - TABLE_SCROLL_OFFSET)) movingState = Right;
            else movingState = Idle;

            if (movingState != Idle && !scrollTimer.isActive()) scrollTimer.start();
        }
        if ((movingState == Idle || !pressed) && scrollTimer.isActive()) scrollTimer.stop();

        return QObject::eventFilter(obj, event);
    }
};

用法

tableView->horizontalHeader()->setSectionsMovable(true);
tableView->horizontalHeader()->setAutoScroll(false);
myEventFilter *filter = new myEventFilter(tableView);
tableView->horizontalHeader()->viewport()->installEventFilter(filter);

关键点

  1. 我增加了一个偏移量,使得滚动不仅在窗口外部开始,而且在窗口内部也开始(在全屏模式下这可能会很有用)
  2. 为了保持相同的滚动速度,我的代码使用QTimersetInterval

<details>
<summary>英文:</summary>

After some improvements to the answer
by Mihoub Abderrahmene Rayene I came up with the following solution:


```c++
// myEventFilter.h

#include &lt;QObject&gt;
#include &lt;QEvent&gt;
#include &lt;QHeaderView&gt;
#include &lt;QTableView&gt;
#include &lt;QScrollBar&gt;
#include &lt;QTimer&gt;

enum MovingState {
    Idle,
    Right,
    Left
};

#define TABLE_SCROLL_OFFSET 0.05

class myEventFilter : public QObject {
Q_OBJECT

public:
    explicit myEventFilter(QTableView *tableView, QObject *parent = nullptr) : tableView(tableView), QObject(parent) {
        scrollTimer.setInterval(10);
        connect(&amp;scrollTimer, &amp;QTimer::timeout, this, &amp;myEventFilter::scrollByInterval);
    }

private:
    bool pressed = false;

    MovingState movingState = Idle;

    QTimer scrollTimer;

    QTableView *tableView;

private slots:

    void scrollByInterval() const {
        int scrollStep = 1;
        QScrollBar *scrollbar = tableView-&gt;horizontalScrollBar();

        if (movingState == Right)
            scrollbar-&gt;setValue(scrollbar-&gt;value() + scrollStep);
        else
            scrollbar-&gt;setValue(scrollbar-&gt;value() - scrollStep);
    };
protected:
    bool eventFilter(QObject *obj, QEvent *event) override {
        if (event-&gt;type() == QEvent::MouseButtonPress) pressed = true;

        if (event-&gt;type() == QEvent::MouseButtonRelease) pressed = false;

        if (event-&gt;type() == QEvent::MouseMove &amp;&amp; pressed) {
            auto *h = dynamic_cast&lt;QHeaderView *&gt;(obj-&gt;parent());

            int cursorPosition = h-&gt;mapFromGlobal(QCursor::pos()).x();
            int tableWidth = tableView-&gt;viewport()-&gt;visibleRegion().boundingRect().width();

            if (cursorPosition &lt;= tableWidth * TABLE_SCROLL_OFFSET) movingState = Left;
            else if (cursorPosition &gt;= tableWidth * (1 - TABLE_SCROLL_OFFSET)) movingState = Right;
            else movingState = Idle;

            if (movingState != Idle &amp;&amp; !scrollTimer.isActive()) scrollTimer.start();
        }
        if ((movingState == Idle || !pressed) &amp;&amp; scrollTimer.isActive()) scrollTimer.stop();

        return QObject::eventFilter(obj, event);
    }
};

Usage

tableView-&gt;horizontalHeader()-&gt;setSectionsMovable(true);
tableView-&gt;horizontalHeader()-&gt;setAutoScroll(false);
myEventFilter *filter = new myEventFilter(tableView);
tableView-&gt;horizontalHeader()-&gt;viewport()-&gt;installEventFilter(filter);

Key points

  1. I added an offset so that scrolling starts not only outside the window, but in it too (this can be useful in full screen mode)
  2. To maintain the same scroll speed, my code uses QTimer along with setInterval

huangapple
  • 本文由 发表于 2023年5月8日 00:28:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76195068.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定