Qt自定义的ColorDialog--仿QColorDialog

news/2025/1/15 15:06:13/

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

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

先看效果,下面的效果分别是自定义的colorWidgetQColorDialog的运行对比。
在这里插入图片描述

在这里插入图片描述

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

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

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

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

首先,我们知道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_OBJECTpublic: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基本完成了。

测试代码。


http://www.ppmy.cn/news/95840.html

相关文章

pta(浙大第四版)五道经典练习题③

目录 ①7-4 IP地址转换 ②、查找日期 ③藏头词 四、IP地址转换 五、删除链表值为偶数的节点 ①7-4 IP地址转换 题述&#xff1a;IP地址转换&#xff1a;一个IP地址是用四个字节&#xff08;每个字节8个位&#xff09;的二进制码组成。输入32位二进制字符串&#xff0c;输…

Apache Kafka - 高性能原因探究

文章目录 概述图解 概述 Kafka 的高性能主要依赖于以下几个关键因素: 分布式架构:Kafka 采用分布式集群架构,可以水平扩展到上万个节点,支持每秒处理百万级消息。持久化存储:Kafka 使用文件系统持久化存储消息,避免了数据库成为性能瓶颈,大大提高了吞吐量。顺序读写:Kafka 的…

基于STM32的DHT11温湿度测量

目录 1.简介 2.主要参数 3.引脚说明 4.注意事项 5.单总线协议 6.数据格式 7.工作时序 8.分模块编写程序 1.简介 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术&#xff0c;确保产品具有极高的可靠…

Unity包围盒

序 比如&#xff0c;目前导入了一个obj文件&#xff0c;想知道它的AABB包围盒是什么。 官方文档 Unity - Scripting API: Bounds (unity3d.com) 可以看到&#xff0c;包围盒有三个类别的&#xff1a; Mesh.bounds Unity - Scripting API: Mesh.bounds (unity3d.com) 不随…

Mysql中的not in和null

MySQL中的not in和null 当我们在MySQL中使用not in时&#xff0c;例如 select id from user when id not in(...)如果not in(…)数据中有null时&#xff0c;返回的结果是空表。 错误在于判断 a not in B的方法的本质是a 使用 ! 与B中的每一条进行判断 比如 3 not in (null…

catia距离测量

选无限 如果有限几何&#xff0c;即默认的情况的话 这距离很多时候可不是我们想要的。

SpringCloudAlibaba中篇(Sentinel,Seata)(超级无敌认真好用,万字收藏篇!!!!)

文章目录 SpringCloudAlibaba中篇(Sentinel,Seata)1 Sentinel&#xff08;流量处理&#xff09;1.1 分布式系统遇到的问题1.2 服务雪崩1.3 容错机制1.4 什么是Sentinel1.5 初步使用Sentinel-流控规则1.6 Sentinel- SentinelResource1.7 初步使用Sentinel-降级规则1.8 控制台部署…

6.2:荷兰国旗问题

文章目录 实现key前面的数都小于等key&#xff0c;key后面的数都大于等于key1&#xff1a;前后指针法&#xff1a;2&#xff1a;挖坑法3&#xff1a;单指针法&#xff08;左神&#xff09; 辗转相除法求最大公约数 实现key前面的数都小于等key&#xff0c;key后面的数都大于等于…