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

问题描述

当鼠标悬停在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();
}

不幸的是,这种行为不是我所期望的。特别是,当我将鼠标移到某个项目上时,会绘制轮廓,但是当我移动到另一个项目时,不会擦除第一个项目周围的轮廓。取而代之的是,它会在我将鼠标移到的所有项目周围绘制轮廓,并最终在所有项目周围绘制轮廓。这正常吗?我知道一种替代解决方案是使用QStyleSheet,但是我想了解为什么当前的方法无法达到我的预期。

这是鼠标悬停之前的小部件外观:

enter image description here

这是将鼠标悬停在item2之后:

enter image description here

然后在第3项之后:

enter image description here

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

编辑1:

根据scopchanov的回答,为确保轮廓厚度确实为1px,我将paint方法更改为:

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

    QStyledItemDelegate::paint(painter,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));
    }
}

不幸的是,这种行为非常相似。这是将所有项目从顶部移到底部之后的屏幕截图:

enter image description here

解决方法

原因

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() + 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)。这看起来很直观,但是让我们看看如果将这些变量替换为实数会发生什么:

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

Pixel zoomed rectangle

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

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

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

结果是修剪轮廓的底部和右侧边缘(这些边缘恰好在绘制区域之内):

Clipped outline

在任何情况下,轮廓线中位于绘制区域之外的部分都不会正确地重新绘制,因此,以前的图形中多余的部分会以线条的形式出现。

解决方案

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

您可能只是写

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

但是,这仅适用于轮廓(宽度为1px,devicePixelRatio为1),例如在PC上。如果轮廓的边界大于1px和/或​​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是类成员,分别代表所需的轮廓宽度。绘画设备的物理像素与独立于设备的像素之间的比率。前提是已为它们创建了公共的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,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粗轮廓产生以下结果:

3px thick outline

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...