1.界面实现效果
以下是具体的项目需要用到的效果展示,用于验证字母。
2.简介
自定义CaptchaMovableLabel,继承自QLabel类:
中间的4个字母,就是CaptchaMovableLabel类来实例化的对象。
主要功能如下:
1.显示字母;
2.实现了鼠标移动事件,使字母可拖动;
3.存在定时器,不断改变字母颜色;
4.绘制字母时,可旋转一定角度;
cpp
#ifndef CAPTCHAMOVABLELABEL_H
#define CAPTCHAMOVABLELABEL_H
#include <QLabel>
#include <QTime>
#include <QPropertyAnimation>
#include <QDebug>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QGraphicsDropShadowEffect>
#include <cmath>
#include <QTimer>
#define CAPTCHA_REFRESH_DURATION 300 // 刷新的动画时长
#define CAPTCHA_CHAR_ANGLE_MAX 20 // 最大旋转角:20°
#define CAPTCHA_SHADOW_BLUR_MAX 80 // 最大的阴影模糊半径
class CaptchaMovableLabel : public QLabel
{
Q_OBJECT
Q_PROPERTY(int refreshProgress READ getRefreshProgress WRITE setRefreshProgress)
Q_PROPERTY(int pressProgress READ getPressProgress WRITE setPressProgress)
public:
CaptchaMovableLabel(QWidget* parent);
void setAngle(int angle);
void setColor(QColor color);
void setText(QString ch);
void startRefreshAnimation();
void setMoveBorder(QRect rect);
QString text();
protected:
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *ev) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void mouseReleaseEvent(QMouseEvent *ev) override;
private:
void startPressAnimation(int end);
void setRefreshProgress(int g);
int getRefreshProgress();
inline bool isNoAni();
void setPressProgress(int g);
int getPressProgress();
private slots:
//设置rgb颜色
void slotMovePos();
private:
QPoint press_pos;
bool dragging = false;
bool moved = false;
QGraphicsDropShadowEffect effect;
QString ch;
QColor color;
int angle = 0;
int refreshProgress = 100;
QString prevCh;
QColor prevColor;
int prevAngle = 0;
QString prevChar;
int pressProgress = 0;
bool inited = false;
QTimer movingTimer;
int moveR, moveG, moveB;
};
#endif // CAPTCHAMOVABLELABEL_H
#include "captchamovablelabel.h"
CaptchaMovableLabel::CaptchaMovableLabel(QWidget *parent) : QLabel(parent)
{
effect.setOffset(0, 0);
// effect.setBlurRadius(8);
setGraphicsEffect(&effect);
movingTimer.setInterval(30);
movingTimer.setSingleShot(false);
connect(&movingTimer, SIGNAL(timeout()), this, SLOT(slotMovePos()));
}
void CaptchaMovableLabel::setAngle(int angle)
{
this->prevAngle = this->angle;
this->angle = angle;
}
void CaptchaMovableLabel::setColor(QColor color)
{
this->prevColor = this->color;
this->color = color;
moveR = qrand() % 5;
moveG = qrand() % 5;
moveB = qrand() % 5;
movingTimer.start();
}
void CaptchaMovableLabel::setText(QString text)
{
this->prevCh = this->ch;
this->ch = text;
// 计算合适的高度
QFontMetrics fm(this->font());
double w = fm.horizontalAdvance(text)+2;
double h = fm.height();
const double PI = 3.141592;
int xieHalf = sqrt(w*w/4+h*h/4); // 斜边的一半
double a = atan(w/h) + CAPTCHA_CHAR_ANGLE_MAX * PI / 180; // 最大的倾斜角度
int w2 = xieHalf * sin(a) * 2;
a = atan(w/h) - CAPTCHA_CHAR_ANGLE_MAX * PI / 180;
int h2 = xieHalf * cos(a) * 2;
resize(w2, h2);
}
void CaptchaMovableLabel::startRefreshAnimation()
{
if (!inited) // 第一次,直接显示,取消动画
{
inited = true;
return ;
}
QPropertyAnimation* ani = new QPropertyAnimation(this, "refreshProgress");
ani->setStartValue(0);
ani->setEndValue(100);
ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION / 3) + CAPTCHA_REFRESH_DURATION / 3);
ani->start();
connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
connect(ani, SIGNAL(finished()), &movingTimer, SLOT(start()));
}
QString CaptchaMovableLabel::text()
{
return ch;
}
void CaptchaMovableLabel::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setFont(this->font());
painter.setRenderHint(QPainter::SmoothPixmapTransform);
int w2 = width()/2, h2 = height()/2;
painter.translate(w2, h2); // 平移到中心,绕中心点旋转
if (isNoAni()) // 不在动画中,直接绘制
{
painter.setPen(color);
painter.rotate(angle);
painter.drawText(QRect(-w2, -h2, width(), height()), Qt::AlignCenter, ch);
return ;
}
// 动画里面,前后渐变替换
double newProp = refreshProgress / 100.0;
double oldProp = 1.0 - newProp;
double a = prevAngle * oldProp + angle * newProp + 0.5;
painter.save();
painter.rotate(a);
QColor c = prevColor;
c.setAlpha(c.alpha() * oldProp); // 旧文字渐渐消失
painter.setPen(c);
painter.drawText(QRect(-w2,-h2,width(),height()), Qt::AlignCenter, prevCh);
c = this->color;
c.setAlpha(c.alpha() * newProp); // 新文字渐渐显示
painter.setPen(c);
painter.drawText(QRect(-w2, -h2, width(), height()), Qt::AlignCenter, ch);
painter.restore();
}
void CaptchaMovableLabel::mousePressEvent(QMouseEvent *ev)
{
if (ev->button() == Qt::LeftButton)
{
// 开始拖拽
press_pos = ev->pos();
dragging = true;
moved = false;
this->raise();
movingTimer.stop();
startPressAnimation(200);
return ev->accept();
}
QLabel::mousePressEvent(ev);
}
void CaptchaMovableLabel::mouseMoveEvent(QMouseEvent *ev)
{
if (dragging && ev->buttons() & Qt::LeftButton)
{
if (!moved && (ev->pos() - press_pos).manhattanLength() < QApplication::startDragDistance())
{
return QLabel::mouseMoveEvent(ev); // 还没到这时候
}
moved = true;
move(this->pos() + ev->pos() - press_pos);
ev->accept();
return ;
}
QLabel::mouseMoveEvent(ev);
}
void CaptchaMovableLabel::mouseReleaseEvent(QMouseEvent *ev)
{
if (dragging)
{
// 结束拖拽
dragging = false;
movingTimer.start();
startPressAnimation(0);
}
if (moved)
return ev->accept();
QLabel::mouseReleaseEvent(ev);
}
void CaptchaMovableLabel::startPressAnimation(int end)
{
QPropertyAnimation* ani = new QPropertyAnimation(this, "pressProgress");
ani->setStartValue(pressProgress);
ani->setEndValue(end);
ani->setDuration(CAPTCHA_REFRESH_DURATION << 1);
ani->start();
connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
}
void CaptchaMovableLabel::setRefreshProgress(int g)
{
this->refreshProgress = g;
update();
}
int CaptchaMovableLabel::getRefreshProgress()
{
return refreshProgress;
}
bool CaptchaMovableLabel::isNoAni()
{
return refreshProgress == 100;
}
void CaptchaMovableLabel::setPressProgress(int g)
{
this->pressProgress = g;
double off = g / 100;
effect.setBlurRadius(g / 20.0);
effect.setOffset(-off, off);
}
int CaptchaMovableLabel::getPressProgress()
{
return pressProgress;
}
void CaptchaMovableLabel::slotMovePos()
{
if (refreshProgress < 100)
return ;
int val = color.red() + moveR;
if ( val > 255)
{
val = 255;
moveR = - qrand() % 5;
}
else if (val < 0)
{
val = 0;
moveR = - qrand() % 5;
}
color.setRed(val);
val = color.green() + moveG;
if ( val > 255)
{
val = 255;
moveG = - qrand() % 5;
}
else if (val < 0)
{
val = 0;
moveG = - qrand() % 5;
}
color.setGreen(val);
val = color.blue() + moveB;
if ( val > 255)
{
val = 255;
moveB = - qrand() % 5;
}
else if (val < 0)
{
val = 0;
moveB = - qrand() % 5;
}
color.setBlue(val);
}
自定义CaptchaLabel类,此类继承QWidget用于存在上面的4个字母。
主要功能如下:
1.画噪音点,背景上绘制无数个随机颜色的点;
2.画噪音线,这个线条是动态的,随时间更改起渐变颜色,线条位置;
3.鼠标点击,生成随机字母。
cpp
#ifndef CAPTCHALABEL_H
#define CAPTCHALABEL_H
#include "captchamovablelabel.h"
#include <QTimer>
#define CAPTCHAR_COUNT 4 // 验证码字符数量
class CaptchaLabel : public QWidget
{
Q_OBJECT
Q_PROPERTY(int refreshProgress READ getRefreshProgress WRITE setRefreshProgress)
public:
CaptchaLabel(QWidget* parent = nullptr);
void refresh();
bool match(QString input);
private:
void initView();
void initData();
void setRefreshProgress(int g);
int getRefreshProgress();
bool isNoAni();
private slots:
void moveNoiseLines();
protected:
void paintEvent(QPaintEvent* ) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
CaptchaMovableLabel* charLabels[CAPTCHAR_COUNT]; // Label控件
QList<QPoint> noisePoints; // 噪音点
QList<QColor> pointColors; // 点的颜色
QList<QPointF> lineStarts; // 噪音线起始点
QList<QPointF> lineEnds; // 噪音先结束点
QList<QPointF> startsV; // 起始点的移动速度(带方向)
QList<QPointF> endsV; // 结束点的速度(带方向)
QList<QColor> lineColor1s; // 线的渐变色1
QList<QColor> lineColor2s; // 线的渐变色2
QList<int> lineWidths;
QTimer movingTimer;
int refreshProgress = 100;
QList<QPoint> noisePoints2; // 新的位置
int autoRefreshMax = 2; // match错误几次后就自动刷新
int matchFailCount = 0; // match错误次数
int matchFailAndRefreshCount = 0; // 失败且导致刷新的次数,强行刷新
};
#endif // CAPTCHALABEL_H
#include "captchalabel.h"
CaptchaLabel::CaptchaLabel(QWidget *parent) : QWidget(parent)
{
initView();
// 这里延迟,等待布局结束
QTimer::singleShot(0, [=]{
initData();
refresh();
});
}
void CaptchaLabel::initView()
{
// 初始化控件
for (int i = 0; i < CAPTCHAR_COUNT; i++)
{
charLabels[i] = new CaptchaMovableLabel(this);
charLabels[i]->move(0, 0);
}
// 初始化时钟
movingTimer.setInterval(30);
movingTimer.setSingleShot(false);
movingTimer.start();
connect(&movingTimer, SIGNAL(timeout()), this, SLOT(moveNoiseLines()));
}
void CaptchaLabel::initData()
{
// 初始化噪音线
auto getRandomColor = [=]{
return QColor(qrand() % 255, qrand() % 255, qrand() % 255);
};
int w = width(), h = height();
int count = 20/*w * h / 400*/;
int penW = qMin(w, h) / 15;
for (int i = 0; i < count; i++)
{
lineStarts.append(QPointF(qrand() % w, qrand() % h));
lineEnds.append(QPointF(qrand() % w, qrand() % h));
startsV.append(QPointF((qrand() % 30 - 15) / 10.0, (qrand() % 30 - 15) / 10.0));
endsV.append(QPointF((qrand() % 30 - 15) / 10.0, (qrand() % 30 - 15) / 10.0));
lineWidths.append(qrand() % penW + 1);
lineColor1s.append(getRandomColor());
lineColor2s.append(getRandomColor());
}
}
void CaptchaLabel::setRefreshProgress(int g)
{
this->refreshProgress = g;
update();
}
int CaptchaLabel::getRefreshProgress()
{
return refreshProgress;
}
bool CaptchaLabel::isNoAni()
{
return refreshProgress == 100;
}
void CaptchaLabel::moveNoiseLines()
{
int w = width(), h = height();
double vBase = 100.0; // 大概最快要3秒钟走完
for (int i = 0; i < lineStarts.size(); i++)
{
QPointF& pos = lineStarts[i];
pos += startsV.at(i);
if (pos.x() < 0)
startsV[i].setX(qrand() % w / vBase);
else if (pos.x() > w)
startsV[i].setX(- qrand() % w / vBase);
if (pos.y() < 0)
startsV[i].setY(qrand() % h / vBase);
else if (pos.y() > h)
startsV[i].setY(- qrand() % h / vBase);
}
for (int i = 0; i < lineEnds.size(); i++)
{
QPointF& pos = lineEnds[i];
pos += endsV.at(i);
if (pos.x() < 0)
endsV[i].setX(qrand() % w / vBase);
else if (pos.x() > w)
endsV[i].setX(- qrand() % w / vBase);
if (pos.y() < 0)
endsV[i].setY(qrand() % h / vBase);
else if (pos.y() > h)
endsV[i].setY(- qrand() % h / vBase);
}
update();
}
void CaptchaLabel::refresh()
{
int width = this->width();
int height = this->height();
// 清空全部内容
for (int i = 0; i < CAPTCHAR_COUNT; i++)
charLabels[i]->hide();
refreshProgress = -1;
update();
// 获取背景底色
QPixmap rend(this->size());
render(&rend);
QColor bgColor = rend.toImage().pixelColor(width/2, height/2);
int br = bgColor.red(), bg = bgColor.green(), bb = bgColor.blue();
// 开始随机生成
const int border = 10;
int leftest = width / border;
int topest = height / border;
int wid = width - leftest * 2;
int hei = height - topest * 2;
for (int i = 0; i < CAPTCHAR_COUNT; i++)
{
auto label = charLabels[i];
// 随机大小
QFont font;
font.setPointSize( qrand() % 8 + 22 );
label->setFont(font);
// 随机旋转
label->setAngle( qrand() % (CAPTCHA_CHAR_ANGLE_MAX*2) - CAPTCHA_CHAR_ANGLE_MAX);
// 生成随机字符
const QString pool = "QWERTYUIOPASDFGHJKLZXCVBNM";
QChar rc = pool.at(qrand() % pool.size());
// 此时会调整大小,setText必须在setFont之后
label->setText(rc);
// 生成随机位置(排除边缘)
int left = leftest + wid * i / CAPTCHAR_COUNT;
int right = leftest + wid * (i+1) / CAPTCHAR_COUNT - label->width();
int x = qrand() % qMax(right-left, 1) + left;
int y = qrand() % qMax(hei - label->height(), 1) + topest;
label->show(); // 之前是hide状态
QPropertyAnimation * ani = new QPropertyAnimation(label, "pos");
ani->setStartValue(label->pos());
ani->setEndValue(QPoint(x, y));
ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION/2) + CAPTCHA_REFRESH_DURATION/2);
ani->setEasingCurve(QEasingCurve::OutQuart);
ani->start();
connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
// 生成随机颜色,且必须和背景颜色有区分度
QColor color;
while (true)
{
int r = qrand() % 255;
int g = qrand() % 255;
int b = qrand() % 255;
if (abs(r-br) + abs(g-bg) + abs(b-bb) > 383)
{
color = QColor(r, g, b);
break;
}
}
label->setColor(color);
label->startRefreshAnimation();
}
// 生成噪音点
int count = wid * hei / border; // 点的数量
if (noisePoints.size() == 0) // 第一次
{
for (int i = 0; i < count; i++)
{
int x = qrand() % width;
int y = qrand() % height;
noisePoints.append(QPoint(x, y / 2));
noisePoints2.append(QPoint(x, y));
pointColors.append(QColor(qrand() % 255, qrand() % 255, qrand() % 255));
}
}
else
{
noisePoints = noisePoints2;
count = noisePoints.size();
noisePoints2.clear();
for (int i = 0; i < count; i++)
{
noisePoints2.append(QPoint(qrand() % width, qrand() % height));
}
}
// 生成噪音线
QPropertyAnimation* ani = new QPropertyAnimation(this, "refreshProgress");
ani->setStartValue(0);
ani->setEndValue(100);
ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION) + CAPTCHA_REFRESH_DURATION);
ani->start();
connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
}
/**
* 判断能否匹配
*/
bool CaptchaLabel::match(QString input)
{
// 根据label的位置排序
std::sort(charLabels, charLabels+CAPTCHAR_COUNT, [=](QLabel* a, QLabel* b){
if (a->pos().x() == b->pos().x())
return a->pos().y() < b->pos().y();
return a->pos().x() < b->pos().x();
});
// 按顺序组合成新的字符串
QString captcha;
for (int i = 0; i < CAPTCHAR_COUNT; i++)
captcha += charLabels[i]->text();
// 进行比较
if (input.toUpper() == captcha)
return true;
// 记录失败
matchFailCount++;
if (matchFailCount >= autoRefreshMax // 达到刷新的次数
|| matchFailAndRefreshCount > 2) // 多次错误导致刷新
{
refresh();
matchFailAndRefreshCount++;
matchFailCount = 0;
}
return false;
}
void CaptchaLabel::paintEvent(QPaintEvent *)
{
QPainter painter(this);
if (refreshProgress == -1) // 不画,可能需要获取背景颜色
return ;
// 画噪音点
if (isNoAni())
{
// 显示随机的点
for (int i = 0; i < noisePoints2.size(); i++)
{
painter.setPen(pointColors.at(i));
painter.drawPoint(noisePoints2.at(i));
}
}
else
{
// 动画过程中的点的移动
double newProp = refreshProgress / 100.0;
double oldProp = 1.0 - newProp;
int count = qMin(noisePoints.size(), noisePoints2.size());
for (int i = 0; i < count; i++)
{
QPoint pt1 = noisePoints.at(i);
QPoint pt2 = noisePoints2.at(i);
QPoint pt( pt1.x() * oldProp + pt2.x() * newProp,
pt1.y() * oldProp + pt2.y() * newProp );
painter.setPen(pointColors.at(i));
painter.drawPoint(pt);
}
}
// 画噪音线
painter.setRenderHint(QPainter::Antialiasing);
for (int i = 0; i < lineStarts.size(); i++)
{
QLinearGradient grad(lineStarts.at(i), lineEnds.at(i));
grad.setColorAt(0, lineColor1s.at(i));
grad.setColorAt(1, lineColor2s.at(i));
painter.setPen(QPen(grad, lineWidths.at(i)));
painter.drawLine(lineStarts.at(i), lineEnds.at(i));
}
}
void CaptchaLabel::mouseReleaseEvent(QMouseEvent *event)
{
if (QRect(0,0,width(),height()).contains(event->pos()))
refresh();
QWidget::mouseReleaseEvent(event);
}
3.使用
新建MainWindow,拖动一个QWidget,提升为CaptchaLabel即可。