将鼠标悬停在 QListWidget 项目上时如何绘制轮廓?

How to paint an outline when hovering over a QListWidget item?

当鼠标悬停在 QListWidget 项目上时,我试图在该项目周围绘制轮廓。我已经将 QStyledItemDelegate 子类化并覆盖 paint 以说明 QStyle::State_MouseOver 情况,如下所示:

class MyDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:

    MyDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent){}

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QStyledItemDelegate::paint(painter, option, index);
        if(option.state & QStyle::State_MouseOver) painter->drawRect(option.rect);
    }

    ~MyDelegate(){}
};

然后我用一些项目实例化一个 QListWidget 并启用 Qt::WA_Hover 属性:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QListWidget w;
    w.addItems(QStringList{"item1", "item2", "item3", "item4"});
    w.setItemDelegate(new MyDelegate(&w));
    w.viewport()->setAttribute(Qt::WA_Hover);
    w.show();
    return a.exec();
}

不幸的是,行为不是我所期望的。特别是,当我将鼠标移到一个项目上时会绘制轮廓,但当我移动到另一个项目时,第一个项目周围的轮廓不会被删除。相反,它一直在我将鼠标移到所有项目周围绘制轮廓,最终所有项目周围都有一个轮廓。这是正常的吗?我知道另一种解决方案是使用 QStyleSheets,但我想了解为什么当前的方法不符合我的预期。

这是小部件在鼠标悬停之前的样子:

鼠标悬停在 item2 上之后的样子:

然后在第 3 项之后:

我在 MacOS 10.15.6 平台上使用 Qt 5.15.1。

编辑 1:

根据scopchanov的回答,为了确保轮廓粗细确实是1px,我把paint方法改成这样:

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
    int outlineWidth = 1;
    QPen pen;
    pen.setWidth(outlineWidth);
    painter->setPen(pen);

    QStyledItemDelegate::paint(painter, option, index);
    if(option.state & QStyle::State_MouseOver) {

        int a = round(0.5*(outlineWidth - 1));
        int b = round(-0.5*outlineWidth);

        painter->drawRect(option.rect.adjusted(a, a, b, b));
    }
}

不幸的是,行为非常相似;这是从上到下将鼠标悬停在所有项目上后的屏幕截图:

原因

QPainter::drawRect 绘制一个矩形,它比绘制区域稍大(高度和宽度恰好一个像素)。可以在 the way QPaintEngine draws a rectangle:

中查看此行为的原因
for (int i=0; i<rectCount; ++i) {
    QRectF rf = rects[i];
    QPointF pts[4] = { QPointF(rf.x(), rf.y()),
                       QPointF(rf.x() + rf.width(), rf.y()),
                       QPointF(rf.x() + rf.width(), rf.y() + rf.height()),
                       QPointF(rf.x(), rf.y() + rf.height()) };
    drawPolygon(pts, 4, ConvexMode);
}

QPaintEngine 绘制一个闭合多边形,从点 (x, y) 开始,到 (x + width, y),然后到 (x + width, y + height),最后到 (x, y + height)。这看起来很直观,但让我们看看如果我们用实数替换这些变量会发生什么:

比如说,我们想在 (0, 0) 处绘制一个 4x2 像素的矩形。 QPaintEngine 将使用以下坐标:(0, 0)(4, 0)(4, 2)(0, 2)。以像素表示,绘图将如下所示:

因此,我们最终得到一个 5x3 像素的矩形,而不是 4x2 像素,即实际上是一个像素宽和高。

您可以通过在调用 drawRect 之前将画家裁剪到 option.rect 来进一步证明这一点:

if (option.state & QStyle::State_MouseOver) {
    painter->setClipRect(option.rect);
    painter->drawRect(option.rect);
}

结果是裁剪了轮廓的底部和右侧边缘(边缘,我们预计在绘制区域内):

在任何情况下,落在绘制区域之外的轮廓部分都没有正确重新绘制,因此以前绘制的线条形式的不需要的残留物。

解决方案

减少轮廓的高度和宽度,使用QRect::adjusted

你可以写

painter->drawRect(option.rect.adjusted(0, 0, -1, -1));

然而,这只适用于轮廓,它是 1px 厚,devicePixelRatio 是 1,就像在 PC 上一样。如果轮廓的边框比 1px and/or 更粗 devicePixelRatio 是 2,就像在 Mac 上一样,当然更多的轮廓会伸出绘制区域,所以你应该考虑到这一点并相应地调整矩形,例如:

int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);

painter->drawRect(option.rect.adjusted(tl, tl, br, br));

m_outineWidthm_devicePixelRatio 是 class 成员,分别代表所需的轮廓宽度。绘制设备的物理像素与 device-independent 像素之间的比率。如果你已经为它们创建了 public setter 方法,你可以像这样设置它们的值:

auto *delegate = new MyDelegate(&w);

delegate->setOutlineWidth(1);
delegate->setDevicePixelRatio(w.devicePixelRatio());

w.setItemDelegate(delegate);

示例

这是我为您编写的示例,用于演示如何实施建议的解决方案:

#include <QApplication>
#include <QStyledItemDelegate>
#include <QListWidget>
#include <QPainter>

class MyDelegate : public QStyledItemDelegate
{
    int m_outineWidth;
    int m_devicePixelRatio;
public:
    
    MyDelegate(QObject *parent = nullptr) :
        QStyledItemDelegate(parent),
        m_outineWidth(1),
        m_devicePixelRatio(1) {
    }
    
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        QStyledItemDelegate::paint(painter, option, index);
        
        if (option.state & QStyle::State_MouseOver) {
            int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
            int tl = round(0.5*(effectiveOutlineWidth - 1));
            int br = round(-0.5*effectiveOutlineWidth);
            
            painter->setPen(QPen(QBrush(Qt::red), m_outineWidth, Qt::SolidLine,
                                 Qt::SquareCap, Qt::MiterJoin));
            painter->drawRect(option.rect.adjusted(tl, tl, br, br));
        }
    }
    
    void setOutlineWidth(int outineWidth) {
        m_outineWidth = outineWidth;
    }
    
    void setDevicePixelRatio(int devicePixelRatio) {
        m_devicePixelRatio = devicePixelRatio;
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QListWidget w;
    auto *delegate = new MyDelegate(&w);
    
    delegate->setOutlineWidth(3);
    delegate->setDevicePixelRatio(w.devicePixelRatio());
    
    w.setItemDelegate(delegate);
    w.addItems(QStringList{"item1", "item2", "item3", "item4"});
    w.viewport()->setAttribute(Qt::WA_Hover);
    w.show();
    
    return a.exec();
}

结果

所提供的示例在 Windows 上生成以下 3px 粗轮廓的结果: