Qt已经有了色板选择,但是它使用QDialog形成的,每次调用基本上都成了点一个按钮,谈一个模态框,选择好颜色之后再关掉模态框。
但是,如果想将颜色选择板放在窗口上,并不会有模态的功能就会比较麻烦,所以为了这个目的,就再造一次轮子。
先看效果,下面的效果分别是自定义的colorWidget
好QColorDialog
的运行对比。
刚开始时,确实有种无处下手的感觉,后来突然想想,当你不会的时候,如果手边刚好有现成的差不多的东西,不仿去抄一抄,所以,我就看了下QColorDialog
的运行方式,甚至去翻了下它的源码。
所以,编写一个程序将QColorDialog
调出来,运行一下,看看他的运行过程,看能不能从中找到一点蛛丝马迹。
研究了一会,从它的运行过程中,我们能够很明显的得到以下几个结论:
- 鼠标在颜色幕布上滑动的时候,只改变它的 Hue和Sat的值,其Val值由右边的slider改变;
- 当鼠标在颜色幕布左上角时,Hue和Sat最大,对应右下角时最小;
- Hue的最大值为359,最小值为0,其他的范围都是0-255;
- 手动改变对应数值输入框的值,鼠标对应的十字线相应改变。
首先,我们知道color的HSV空间的数值就是0-360的区间,如下图所示:
那么,这个鼠标选择颜色的幕布刚好就是将这个空间从0和359这个点剪开之后展平了。
我们今天仿生的部分就是QColorDialog
界面的右边部分,左边部分相对来说是比较简单的。
从这半部分界面来看,我们需要克服的难题,如何画出从左到右并且从下到上的渐变色。因为我们都知道,Qt是有一个QGradient
类来绘制渐变色的,并且在这个类下面派生了三个已经现成的渐变方案。QConicalGradient, QLinearGradient, and QRadialGradient
。而QLinearGradient
刚好能够满足我们的需求。
所以,第一遍,我想到的就是用两个QWidget
来叠放,然后设置两个QWidget
的背景色为对应的渐变色。这样,这两个背景色的叠加显示效果就能够满足我们的视觉效果了。也就是下面这样。
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 0, 1, 255), stop:0.167 rgba(255, 0, 255, 255), stop:0.333 rgba(0, 0, 255, 255), stop:0.5 rgba(0, 255, 255, 255), stop:0.667 rgba(0, 255, 0, 255), stop:0.833 rgba(255, 255, 60, 255), stop:1 rgba(255, 0, 0, 255));
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(0, 0, 0, 0), stop:1 rgba(255, 255, 255, 255));
这样来看,效果是满足的,但是要实现后面的鼠标拖动绘制十字线的话,还是要重新一个QWidget
来做绘制类,那何不一次性全用这个呢?
所以,就有了下面这个类。
class ColorCoustomWidget : public QWidget
{
Q_OBJECT
public:
explicit ColorCoustomWidget(QWidget *parent = nullptr);
~ColorCoustomWidget();
void setColor(const QColor& color);
protected:
void paintEvent(QPaintEvent *event) override;
void showEvent(QShowEvent *event) override;
private:
void initPage();
void drawBrush(QMouseEvent *event);
signals:
void signal_color(const QVariant& data);
private:
QPointF m_ptPointer;
int m_val;
};
这个类的主要作用是用来实现颜色幕布的绘制、鼠标拖动时的十字线绘制及传递出去QColor
HSV空间的两个数值。
有一个成员变量,用来表示十字线的原点,另一个成员变量用来表示HSV空间的Value值。
首先通过paintEvent
函数来绘制幕布。
void ColorCoustomWidget::paintEvent(QPaintEvent *event)
{
QImage back(size(), QImage::Format_ARGB32);
back.fill(Qt::transparent);
QPainter painter;
painter.begin(&back);
QPen pen = QPen(Qt::black, 2, Qt::SolidLine, Qt::RoundCap);
painter.setPen(pen);
QLineF line1(m_ptPointer.x() - 5, m_ptPointer.y(), m_ptPointer.x() + 5, m_ptPointer.y());
QLineF line2(m_ptPointer.x(), m_ptPointer.y() - 5, m_ptPointer.x(), m_ptPointer.y() + 5);
painter.drawLine(line1);
painter.drawLine(line2);
painter.end();
painter.begin(this);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
QLinearGradient linearGradientH(this->rect().topLeft(), this->rect().topRight());
linearGradientH.setSpread(QGradient::PadSpread);
linearGradientH.setColorAt((qreal)0, QColor(255, 0, 1, 255));
linearGradientH.setColorAt((qreal)1 / 6, QColor(255, 0, 255, 255));
linearGradientH.setColorAt((qreal)1 / 3, QColor(0, 0, 255, 255));
linearGradientH.setColorAt((qreal)1 / 2, QColor(0, 255, 255, 255));
linearGradientH.setColorAt((qreal)2 / 3, QColor(0, 255, 0, 255));
linearGradientH.setColorAt((qreal)5 / 6, QColor(255, 255, 0, 255));
linearGradientH.setColorAt(1, QColor(255, 0, 0, 255));
painter.fillRect(this->rect(), linearGradientH);
QLinearGradient linearGradientV(this->rect().topLeft(), this->rect().bottomLeft());
linearGradientV.setColorAt(0, QColor(0, 0, 0, 0));
linearGradientV.setColorAt(1, QColor(255, 255, 255, 255));
linearGradientV.setSpread(QGradient::PadSpread);
painter.fillRect(this->rect(), linearGradientV);
painter.drawImage(0, 0, back);
painter.end();
}
绘制完的效果跟前面叠加两个QWidget
的效果是一样的。
鼠标事件的最终效果是用来确定成员变量的m_ptPointer
的坐标。然后通过坐标绘制两条相交的长度为10的黑色线段。然后向上级emit
一个改变颜色的信号。
void ColorCoustomWidget::drawBrush(QMouseEvent *event)
{
if (event->type() == QEvent::MouseButtonPress)
{
m_ptPointer = event->pos();
}
else if (event->type() == QEvent::MouseMove)
{
m_ptPointer = event->pos();
}
else if (event->type() == QEvent::MouseButtonRelease)
{
}
m_ptPointer.setX(qMax(0, qMin((int)m_ptPointer.rx(), size().width())));
m_ptPointer.setY(qMax(0, qMin((int)m_ptPointer.ry(), size().height())));
update();
QColor t;
t.setHsv(359* (1 - (qreal)m_ptPointer.rx() / size().width()), 255 * (1 - (qreal)m_ptPointer.ry() / size().height()), m_val);
emit signal_color(t);
}
设置颜色值的时候需要进行一次转换,因为这个HSV空间的最大值是359和255,所以要根据界面的大小转换成在HSV空间对应的数值。
上级界面就是模仿QColorDialog
,通过一个QSlider
来模仿一个滑动块,通过留个QSpinBox分别表示颜色的各个数值。
接收ColorCoustomWidget
类传上来的信号之后,设置界面的数值。
connect(ui->wdgCoustom, &ColorCoustomWidget::signal_color, this, [this](const QVariant& data)
{
updateColor(data.value<QColor>());
});
void ColorWidget::updateColor(const QColor &color)
{
ui->spinRed->setValue(color.red());
ui->spinGreen->setValue(color.green());
ui->spinBlue->setValue(color.blue());
ui->spinHue->setValue(color.hsvHue());
ui->spinSat->setValue(color.hsvSaturation());
ui->spinVal->setValue(color.value());
ui->lineEdit->setText(color.name(QColor::HexRgb));
ui->wdgColor->setStyleSheet(QString("background:%1;").arg(ui->lineEdit->text()));
QColor t;
t.setHsv(color.hsvHue(), color.hsvSaturation(), 255);
QString qss(QString("QSlider::groove {top:6px;bottom:6px;right: 6px;background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0 %1, stop:1#000000);}"
"QSlider::handle:vertical{border-image: url(:/resource/slider-handler.png);margin:-6px;}").arg(t.name(QColor::HexRgb)));
ui->sliderVal->setStyleSheet(qss);
}
上面设置界面的时候,是要通过模拟的方式设置QSlide
的样式,这个样式是根据界面的颜色来设置的。因为从底层传上来的QColor
是HSV空间的,并且value值是固定的255;通过QSlider
的滑动来改变value值。
所以就能很方便的确定渐变色的两个点。然后通过qss的方式设置QSlider
的样式表。可以看见的QSlider
右侧的那个三角形是手绘的一个22*9的矩形,设置之后与QSlider
本体重合的部分是透明的,右半部分是一个三角形。
为了能够显示在右侧,需要设置right:6px;
如果不设置这个属性,单纯地设置margin-right:6px;
是不生效的。
这是一个新的知识点,毕竟尝试了很久才达到的效果。
接下来,为了逼真一点,抄的更像一点,我们需要在手动修改QSpinBox
的数值时希望能够修改界面的颜色,并且十字线也能够跟随数值的变化而进行重绘。
所以我们需要,connect
QSpinBox
的 valueChanged
信号,并且重新设置界面。
auto slot_hsv = [this](int val)
{
QColor color;
color.setHsv(ui->spinHue->value(), ui->spinSat->value(), ui->spinVal->value());
updateColor(color);
};
auto slot_rgb = [this](int val)
{
QColor color;
color.setRgb(ui->spinRed->value(), ui->spinGreen->value(), ui->spinBlue->value());
updateColor(color);
};
connect(ui->spinHue, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_hsv);
connect(ui->spinSat, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_hsv);
connect(ui->spinVal, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_hsv);
connect(ui->spinVal, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this](int val)
{
ui->sliderVal->setValue(val);
});
connect(ui->spinRed, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_rgb);
connect(ui->spinGreen, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_rgb);
connect(ui->spinBlue, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, slot_rgb);
这样设置之后,按照预想应该是正常的,没想到的是,运行起来之后,直接堆栈溢出了。因为设置之后,其他的也改变了value,改变了就会发信号,发了信号我们就又设置了,设置了就又发信号,所以就一直在这样的循环中重复了起来。
后面查了setKeyBoardTracking(false);
可以预防这种情况,所以堆每一个QSpinBox
都进行了这样的设置。
设置完之后发现,如果connect
的槽函数是同一个,是生效的,但是我们的六个QSpinBox
是connect
了两个不同的槽函数,所以导致还是会发生上面一样的堆栈溢出的情况发生。
最后使用了一招暴力的解决方式,在两个槽函数中,在设置界面数据之前,对另外三个QSpinBox
进行信号屏蔽,设置完界面之后再取消信号屏蔽。
uto slot_hsv = [this](int val)
{
ui->spinRed->blockSignals(true);
...
QColor color;
color.setHsv(ui->spinHue->value(), ui->spinSat->value(), ui->spinVal->value());
updateColor(color);
ui->spinRed->blockSignals(false);
...
};
上级界面通过手动修改颜色之后,需要更新底层颜色幕布的显示,所以就有了下面的这个函数:
void ColorCoustomWidget::setColor(const QColor& color)
{
int hue = color.hsvHue();
int sat = color.hsvSaturation();
m_val = color.value();
int rx = size().width() * (1 - (qreal)hue / 359);
int ry = size().height() * (1 - (qreal)sat / 255);
m_ptPointer.setX(rx);
m_ptPointer.setY(ry);
update();
}
设置之后也要进行一次反转换,才能将代表颜色的十字线准确的绘制在界面上。
至此,一个照猫画虎的colorWidget
基本完成了。
测试代码。