QT 实现随机码验证

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即可。

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00613 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术13 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript