将鼠标悬停在 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();
}
不幸的是,行为不是我所期望的。特别是,当我将鼠标移到一个项目上时会绘制轮廓,但当我移动到另一个项目时,第一个项目周围的轮廓不会被删除。相反,它一直在我将鼠标移到所有项目周围绘制轮廓,最终所有项目周围都有一个轮廓。这是正常的吗?我知道另一种解决方案是使用 QStyleSheet
s,但我想了解为什么当前的方法不符合我的预期。
这是小部件在鼠标悬停之前的样子:
鼠标悬停在 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_outineWidth
和 m_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 粗轮廓的结果:
当鼠标悬停在 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
s,但我想了解为什么当前的方法不符合我的预期。
这是小部件在鼠标悬停之前的样子:
鼠标悬停在 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_outineWidth
和 m_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 粗轮廓的结果: