1.界面实现效果
以下是具体的项目需要用到的效果展示,用于验证字母。
2.简介
自定义CaptchaMovableLabel,继承自QLabel类:
中间的4个字母,就是CaptchaMovableLabel类来实例化的对象。
主要功能如下:
1.显示字母;
2.实现了鼠标移动事件,使字母可拖动;
3.存在定时器,不断改变字母颜色;
4.绘制字母时,可旋转一定角度;
#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.鼠标点击,生成随机字母。
#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即可。