Qt自定义的ColorDialog--仿QColorDialog

Qt已经有了色板选择,但是它使用QDialog形成的,每次调用基本上都成了点一个按钮,谈一个模态框,选择好颜色之后再关掉模态框。

但是,如果想将颜色选择板放在窗口上,并不会有模态的功能就会比较麻烦,所以为了这个目的,就再造一次轮子。

先看效果,下面的效果分别是自定义的colorWidgetQColorDialog的运行对比。
Qt自定义的ColorDialog--仿QColorDialog_第1张图片

Qt自定义的ColorDialog--仿QColorDialog_第2张图片

刚开始时,确实有种无处下手的感觉,后来突然想想,当你不会的时候,如果手边刚好有现成的差不多的东西,不仿去抄一抄,所以,我就看了下QColorDialog的运行方式,甚至去翻了下它的源码。

所以,编写一个程序将QColorDialog调出来,运行一下,看看他的运行过程,看能不能从中找到一点蛛丝马迹。

研究了一会,从它的运行过程中,我们能够很明显的得到以下几个结论:

  1. 鼠标在颜色幕布上滑动的时候,只改变它的 Hue和Sat的值,其Val值由右边的slider改变;
  2. 当鼠标在颜色幕布左上角时,Hue和Sat最大,对应右下角时最小;
  3. Hue的最大值为359,最小值为0,其他的范围都是0-255;
  4. 手动改变对应数值输入框的值,鼠标对应的十字线相应改变。

首先,我们知道color的HSV空间的数值就是0-360的区间,如下图所示:
Qt自定义的ColorDialog--仿QColorDialog_第3张图片

那么,这个鼠标选择颜色的幕布刚好就是将这个空间从0和359这个点剪开之后展平了。

我们今天仿生的部分就是QColorDialog界面的右边部分,左边部分相对来说是比较简单的。

从这半部分界面来看,我们需要克服的难题,如何画出从左到右并且从下到上的渐变色。因为我们都知道,Qt是有一个QGradient类来绘制渐变色的,并且在这个类下面派生了三个已经现成的渐变方案。QConicalGradient, QLinearGradient, and QRadialGradient。而QLinearGradient刚好能够满足我们的需求。

所以,第一遍,我想到的就是用两个QWidget来叠放,然后设置两个QWidget的背景色为对应的渐变色。这样,这两个背景色的叠加显示效果就能够满足我们的视觉效果了。也就是下面这样。
Qt自定义的ColorDialog--仿QColorDialog_第4张图片

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 QSpinBoxvalueChanged 信号,并且重新设置界面。

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的槽函数是同一个,是生效的,但是我们的六个QSpinBoxconnect了两个不同的槽函数,所以导致还是会发生上面一样的堆栈溢出的情况发生。

最后使用了一招暴力的解决方式,在两个槽函数中,在设置界面数据之前,对另外三个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基本完成了。

测试代码。

你可能感兴趣的:(Qt,qt,开发语言,colorDialog,自定义组件,QColorDialog)