2021-08-15

本文档介绍了一种使用QPainter绘制甘特图的方法,包括随机生成直线模拟甘特图、图像缓冲绘制、橡皮筋缩放以及直线击中算法。此外,还实现了在大量直线对象中快速检索和显示Tips的功能。通过单元格分区和多值哈希表优化了检索性能。提供了完整的Qt代码实现。

使用QPainter绘制实现类甘特图,实现绘制画面缩放,实现直线击中算法。

近期因为工作原因,需要实现无线频谱的跳频检测功能,本想使用QCustomplot来实现类似甘特图绘制的,经过试验发现QCustomplot组件未包含该部分样例,后查找资料,发现Qt 相关的甘特图组件KDCHART,经过使用发现也不能满足要求,考虑良久,干脆自己实现,因此就存在来这个样例。当然这个样例程序离实际应用还有一定距离,但是可以给一个思路参考。

本样例程序实现功能要点

1、QPainter实现甘特图绘制(使用随机直线代替甘特图Item,需要扩充则更改直线为矩形Item)。
2、实现对绘制图形的橡皮筋缩放(包含局部缩放)。
3、实现对象的击中算法(这里实现仅仅实现直线击中:注意该直线击中是任意直线)
4、实现在绘制对象超多时的快速检索算法。
5、实现击中对象的Tips功能。

本样例效果:

原始生成并显示随机直线
在这里插入图片描述
鼠标击中直线对象后显示对应的直线tips
在这里插入图片描述
缩放处理演示
原始缩放状态(注意:这里打开了单元格线标志)
在这里插入图片描述
缩放后展示(注意:仅仅实现来X轴缩放, y轴类似)
在这里插入图片描述
缩放状态下的击中直线tips展示
在这里插入图片描述

1) 对象生成

首先我们要绘制,那么先需要存在对象,因此我们使用随机算法,随机生成一些直线对象,然后保存起来。

QVector m_vecLines;
void randGerneratLines(); //随机生成直线数据

void Widget::randGerneratLines()
{

	    int linesNumb = qrand() % 1024;                 //随机确认绘制的线数目
	
	    QVector <QLineF>        tLines;
	    tLines.resize(linesNumb);
	
	    int w = width();
	    int h = height();
	
	    for (int i=0; i<linesNumb; ++i)
	    {
	        int x = qrand() % w;
	        int y = qrand() % h;
	        int xoff =qrand() % 40;
	        QLineF  tLine(QPointF(x, y),QPointF(x+xoff, y));                //建立直线对象
	        tLines[i] = tLine;                                              //保存直线对象到一个向量中
	    }
	    setDataLines (tLines);                          //初始化来数据后,就将直线数据传输到成员,并且绘制原始的Image
}

2)画板绘制

我们要提高绘制性能,需要使用图像缓冲方式绘制,如果直接在QPainter上绘制,在绘制对象非常多的情况下则性能会非常低下,导致界面卡顿。因此我们使用QImage作为一个画板来先行绘制。

QImage* m_pImage; //绘制的原始图片
绘制函数
void drawOrgBimp();

void Widget::drawOrgBimp()
{

	    //绘制原始的图像
	    QPainter painter(m_pImage);
	    painter.setRenderHint(QPainter::Antialiasing);
	    painter.save();
	    //设置绘制线笔颜色
	    painter.setPen(Qt::white);  //QColor(123,235,12));
	    //绘制背景颜色
	    painter.fillRect(m_pImage->rect(), QBrush(QColor(0,0,0)));
	
	    int size = m_vecLines.size();
	    for (int i = 0; i<size; ++i)
	    {
	        painter.drawLine(m_vecLines[i]);
	    }
	
	    //绘制单元格线
	    if(m_bDrawCellLine)
	    {
	        painter.setPen(QColor(23,225,12));
	        int w = width();
	        int h = height();
	        for (int i=0; i<10; ++i)
	        {
	            painter.drawLine(QPointF(0,i*m_yCalibrationBase), QPointF(w, i*m_yCalibrationBase));
	            painter.drawLine(QPointF(i*m_xCalibrationBase,0), QPointF(i*m_xCalibrationBase, h));
	        }
	    }
	
	    //绘制单元格索引文本
	    if (m_bDrawCellIndex)
	    {
	        for (int row=0;row<10; row++)
	        {
	            for (int col=0;col<10;col++)
	            {
	                painter.drawText(QPointF(col*m_xCalibrationBase+m_xCalibrationBase/2, row*m_yCalibrationBase+m_yCalibrationBase/2), QString("%1,%2").arg(row).arg(col));
	            }
	        }
	    }
}

3)实际绘制显示

在我们绘制好原始图后,我们需要实际的绘制到我们的屏幕上。
因此我们覆盖实现 void paintEvent(QPaintEvent *event) override; 函数

	void Widget::paintEvent(QPaintEvent *event)
	{
	
		    if (!m_firstDraw)
		    {
		        drawOrgBimp();
		        m_pLocalImage = m_pImage->copy(0,0, m_pImage->width()*m_xScaleRate, m_pImage->height()*m_yScaleRate);
		    }
		
		    QPainter painterThis(this);
		    QTransform transform = painterThis.transform();
		    transform.translate(-m_startX*m_xScaleRate, 0);             //偏移缩放区的起点部分
		    painterThis.setTransform(transform);
		    painterThis.drawImage(0, 0, m_pLocalImage.scaled(m_pLocalImage.width()*m_xScaleRate,height()*m_yScaleRate,Qt::IgnoreAspectRatio));
		    qDebug() << "m_pLocalImage.width()=" << m_pLocalImage.width();
		    
	}

在该函数中真实绘制到屏幕上使用来另外一个Image对象,使用该对象进行实际的屏幕显示是因为我们实现缩放功能所需。

4)缩放准备

我们完成来直线对象生成,绘制,接下来我们来实现缩放功能。要实现缩放功能,那么首先必然存在X轴缩放因子,Y轴缩放因子,因此我们定义成员:
double                      m_xScaleRate =1.0;                               //X向缩放比例
double                      m_yScaleRate =1.0;                               //Y向缩放比例

在我们绘制到屏幕时,我们采用的是使用一个本地Image从原始底图中拷贝显示内容,然后绘制到屏幕上,此时使用缩放因子来完成真实绘制计算,具体参看  	 3)中内容。

5)橡皮筋实现

我们要进行缩放控制,本样例采用的是鼠标橡皮筋方式,正向拖拽橡皮筋是放大,反向橡皮筋选择则是还原。
我们要实现橡皮筋,那么我们需要使用QRubberBand类。
定义成员:
QRubberBand *               mRubberBand;                                //实现橡皮筋的对象
在构造中初始化列表中
, mRubberBand(new QRubberBand(QRubberBand::Rectangle, this))

然后我们重写

void mousePressEvent(QMouseEvent *event) override;

void mouseMoveEvent(QMouseEvent *event) override;

void mouseReleaseEvent(QMouseEvent *event)override;

//记录鼠标按下点,与标志,设置橡皮筋对象显示
void Widget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
    {
        m_bPressFlag = true;
        mOrigin = event->pos();
        mRubberBand->setGeometry(QRect(mOrigin, QSize()));
        mRubberBand->show();
    }
    QWidget::mousePressEvent(event);
}
//在鼠标按下状态改变橡皮筋大小
void Widget::mouseMoveEvent(QMouseEvent *event)
{
    if (m_bPressFlag)
    {
        if (mRubberBand->isVisible())
        {
            mRubberBand->setGeometry(QRect(mOrigin, event->pos()).normalized());
        }
    }
    else {
        QPointF   curPt = event->pos();
        QLineF line = getObjLineF(curPt);
    }

    QWidget::mouseMoveEvent (event);
}
//在鼠标弹起的位置判断橡皮筋对象是正向还是反向,正向执行放大处理,计算反向则进行缩放还原
void Widget::mouseReleaseEvent(QMouseEvent *event)
{
    m_bPressFlag = false;
    if (mRubberBand->isVisible())
    {
        if(mOrigin.rx() < event->pos().rx()) {
            const QRect zoomRect = mRubberBand->geometry();
            int xp1, yp1, xp2, yp2;
            zoomRect.getCoords(&xp1, &yp1, &xp2, &yp2);

            qreal rw = width();
            double tScale = rw / zoomRect.width();

            //需要通过当前鼠标点计算真实起点,以及终点,需要通过上一次存在的缩放因子计算,因此在这里要在最新因子计算的前面进行计算
            //当前点除以上次缩放因子加上本次缩放窗口的原起点。
            //真实起点X = 上次起点X + 当前鼠标点X / 缩放因子
            m_startX = m_startX + mOrigin.rx()/m_xScaleRate;                //等于上一次的起点+本次缩放区内部点长
            //缩放因子计算正确,缩放因子=上次缩放因子*本次计算缩放因子
            m_xScaleRate= m_xScaleRate * tScale;
            qreal rh = height();
            qreal ry = rh / zoomRect.height();
            m_yScaleRate = 1.0;
			
            m_pLocalImage = m_pImage->copy(mOrigin.rx(),0, zoomRect.width(), height());
        }
        else {
            //还原只需要把原来记录下来的原始设置重新设置一次就搞定
            m_xScaleRate = 1.0;
            m_yScaleRate = 1.0;
            m_startX = 0.0;
            m_startY = 0.0;
            m_pLocalImage = m_pImage->copy(m_startX, m_startY, m_pImage->width(), m_pImage->height());
        }

        mRubberBand->hide();
        update();
    }
    QWidget::mouseReleaseEvent (event);
}

6)击中实现

接下来我们实现直线击中基础算法。
void calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
                         qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4);
//计算直线的指定(x,y)偏移外围区域

void Widget::calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
                                       qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4)
{
    //竖直
    double xval = abs(x2 - x1);
    //if (abs(x2 - x1) == 0)        //本句原意
    if (!(xval>0 || xval<0))
    {
        if (y2 > y1)
        {
            ax1 = x1 - Vx;
            ay1 = y1 - Hx;
            ax2 = x1 + Vx;
            ay2 = y1 - Hx;
            ax3 = x1 - Vx;
            ay3 = y2 + Hx;
            ax4 = x1 + Vx;
            ay4 = y2 + Hx;
        }
        else
        {
            ax1 = x1 - Vx;
            ay1 = y2 - Hx;
            ax2 = x1 + Vx;
            ay2 = y2 - Hx;
            ax3 = x1 - Vx;
            ay3 = y1 + Hx;
            ax4 = x1 + Vx;
            ay4 = y1 + Hx;
        }
        return;
    }

    qreal k = qreal(y2 - y1)/qreal(x2 - x1);
    qreal angle = atan(k);

    //线段长度的一半
    //float halfLenth = float(sqrt((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1)))/2;

    //中点
    qreal mx = (x2 + x1)/2;
    qreal my = (y2 + y1)/2;

    qreal alf;
    qreal cx1,cy1,cx2,cy2;

    alf = (-1)*angle;

    cx1 = qreal(cos(alf) * (x1 - mx) - sin(alf) * (y1 - my) + mx);
    cy1 = qreal(sin(alf) * (x1 - mx) + cos(alf) * (y1 - my) + my);
    cx2 = qreal(cos(alf) * (x2 - mx) - sin(alf) * (y2 - my) + mx);
    cy2 = qreal(sin(alf) * (x2 - mx) + cos(alf) * (y2 - my) + my);

    qreal bx1, by1,bx2, by2,bx3, by3,bx4, by4;

    if (x1>x2)
    {
         bx1 = cx1 + Hx;
         by1 = cy1 + Vx;
         bx2 = cx1 + Hx;
         by2 = cy1 - Vx;
         bx3 = cx2 - Hx;
         by3 = cy2 + Vx;
         bx4 = cx2 - Hx;
         by4 = cy2 - Vx;
    }
    else
    {
         bx1 = cx1 - Hx;
         by1 = cy1 + Vx;
         bx2 = cx1 - Hx;
         by2 = cy1 - Vx;
         bx3 = cx2 + Hx;
         by3 = cy2 + Vx;
         bx4 = cx2 + Hx;
         by4 = cy2 - Vx;
    }

    qreal theta = angle;
    ax1 = qreal(cos(theta) * (bx1 - mx) - sin(theta) * (by1 - my) + mx);
    ay1 = qreal(sin(theta) * (bx1 - mx) + cos(theta) * (by1 - my) + my);
    ax2 = qreal(cos(theta) * (bx2 - mx) - sin(theta) * (by2 - my) + mx);
    ay2 = qreal(sin(theta) * (bx2 - mx) + cos(theta) * (by2 - my) + my);

    ax3 = qreal(cos(theta) * (bx3 - mx) - sin(theta) * (by3 - my) + mx);
    ay3 = qreal(sin(theta) * (bx3 - mx) + cos(theta) * (by3 - my) + my);
    ax4 = qreal(cos(theta) * (bx4 - mx) - sin(theta) * (by4 - my) + mx);
    ay4 = qreal(sin(theta) * (bx4 - mx) + cos(theta) * (by4 - my) + my);
}
//实际的检测是否击中直线,传入参数_checkline是要检测的直线, _pt则是鼠标点
bool Widget::checkLineHit(const QLineF &_checkline,QPointF _pt)
{
    QPointF   pt = QPointF(_pt.rx(), _pt.ry());
    qreal   x1 = _checkline.p1().rx() ;
    qreal   x2 = _checkline.p2().rx() ;
    qreal   y1 = _checkline.p1().ry() ;
    qreal   y2 = _checkline.p2().ry() ;

    QPolygonF   ploygon;

    qreal vx = 2.0, hx = 6.0;
    qreal rsx1, rsy1, rsx2, rsy2, rsx3, rsy3, rsx4, rsy4;

    //根据直线两点计算直线的外围矩形--与直线包容平行的矩形
    calculateRegionRef(vx, hx, x1, y1, x2, y2, rsx1, rsy1, rsx2, rsy2, rsx3, rsy3, rsx4, rsy4);

    ploygon.append(QPointF(rsx1, rsy1));
    ploygon.append(QPointF(rsx3, rsy3));
    ploygon.append(QPointF(rsx4, rsy4));
    ploygon.append(QPointF(rsx2, rsy2));

    return ploygon.containsPoint(_pt, Qt::OddEvenFill);         // Qt::WindingFill
}

7)快速检索思路

我们实现来直线的击中算法后,通常我们就应该直接的在mouseMove函数中进行击中测试,一般的做法是遍历我们的所有对象进行击中测试,但是这样做效率非常的低下,假如存在10000个对象的情况下,最坏需要循环10000次,因此我们需要进行一个快速检索的算法。
思路:
		a、对象都是存在于屏幕上,通常情况下我们的对象不会大量重叠,因此我们可以将屏幕进行表格化分区,然后将表格化单元格矩形保存到到一个Hash表中,这个hash表的key就是我们的单元格索引标号(行,列)
		b、我们对所有的对象进行分区关联保存,保存到一个Multihash中。也就是对应单元格索引作为key,与该单元格矩形区域相关的对象均保存到这个key值中 (注意直线对象需要检测起点与终点---该算法存在一个问题,当直线太长超过多个单元格时,中间位置不能检索到,但我的应用中直线长度不会超过单元格,最多就是横跨2格单元格)。 
		c、在应用检索时,首先判断鼠标点在单元格的位置,获取到单元格索引,然后通过单元格索引号查找到该单元格关联的直线对象,这样通过一个二级检索,可以大量的减少检索次数,从而提高性能。

8) 快速检索具体实现

成员如下:
	//将宽与高均分成10份,也就是一共10 X项hash,10个Y项hash.
    int                         m_xNumb;                                    //均分等份数目,默认是10
    int                         m_yNumb;                                    //均分等份数目,默认是10
    qreal                       m_xCalibrationBase;                         //等于宽/m_xNumb
    qreal                       m_yCalibrationBase;                         //等于高/m_yNumb
    QHash <QPair<int,int> , QRectF>     m_keyTableHash;                     //用来存储表格单元矩形的hash容器,其中QPair<int,int>是相当于QModelIndex(row, col)
 	//用来记录线对象属于哪一个单元区域的多值hash
	QMultiHash<QPair<int,int>, QLineF >   m_multiHash;                      //每个单元格下会包含的线对象

	void Widget::positionLines()
	{
	   
	    int linesNumb = m_vecLines.size();                 //线对象数目
	    m_multiHash.clear();
	
	    for (int i=0; i<linesNumb; ++i)
	    {
	        //判断线对象起点的所属块索引号
	        int row = getBlockIndex_Row(m_vecLines[i].p1().ry());
	        int col = getBlockIndex_Col(m_vecLines[i].p1().rx());
	        //创建对应单元索引
	        QPair<int, int> pair;
	        pair.first = col;
	        pair.second = row;
	        //根据求出的单元索引区判断该单元是否存在,并且按找到的单元矩形作为key,记录对应的线对象到多值hash中
	        auto it = m_keyTableHash.find(pair);
	        if (it != m_keyTableHash.end())
	        {
	            m_multiHash.insert(pair, m_vecLines[i]) ;
	        }
	        //检查直线的尾部是否也是归属与上面的区域,如果尾部点位置不属于上面单元,则需要把这条直线同时放到下一个单元中
	        int col1 = getBlockIndex_Col(m_vecLines[i].p2().rx());
	        if (col != col1)
	        {
	            QPair<int, int> pair1;
	            pair.first = col1;
	            pair.second = row;
	            auto it = m_keyTableHash.find(pair1);
	            if (it != m_keyTableHash.end())
	            {
	                m_multiHash.insert(pair, m_vecLines[i]) ;
	            }
	        }
	    }
	
	    update ();
	}

9)鼠标移动击中对象后tips实现

我们已经实现了缩放,对象击中,这之后我们需要在鼠标移动到对象(对象被击中时)后,给出tips,因此我们可以直接使用我们的绘图窗口的toolTips功能,在我们的击中位置添加对应的tips就可以完成这个功能。具体位置在mouseMove中

	void Widget::mouseMoveEvent(QMouseEvent *event)
	{
	
		   if (m_bPressFlag)
		   {
		       if (mRubberBand->isVisible())
		       {
		           mRubberBand->setGeometry(QRect(mOrigin, event->pos()).normalized());
		       }
		   }
		   else {
		       QPointF   curPt = event->pos();
		       QLineF line = getObjLineF(curPt); //检测在这里完成
		   }
		
		   QWidget::mouseMoveEvent (event);
	}

	QLineF Widget::getObjLineF(QPointF _pt)
	{
        //当前传入的点坐标需要计算出原来缩放因子1.0时的坐标(X,y),需要加上记录的m_startX
	    double realX = m_startX + _pt.rx()/m_xScaleRate;
	
	    QPointF  xp = QPointF(realX , _pt.ry());
	    int col = getBlockIndex_Col(xp.rx());
	    int row = getBlockIndex_Row(xp.ry());
	
	    qDebug() << "(row,col)->(" << row << "," << col << ")"   << "xp.rx()=" << realX <<  "_pt.ry()=" << _pt.ry();
	
	    QPair<int,int>  curKey (col, row);
	    QList<QLineF> tLines = m_multiHash.values(curKey);
	
	    int size = tLines.size();
	
	    QLineF  line ;
	    for (int i=0; i<size; ++i)
	    {
	         bool hited = checkLineHit(tLines.at(i), xp);
	         if (hited)
	         {
	            line = tLines.at(i);
	            qDebug() << "Hit lines : " << "rx1=" << line.p1().rx() << "ry1=" << line.p1().ry() << " rx2=" << line.p2().rx() << "ry2=" << line.p1().ry() ;
	
	            QString  str = "Hit lines : ";
	            str  += QString::number(line.p1().rx(), 'f' , 2);
	            str += "ry1=";
	            str += QString::number(line.p1().ry(), 'f' , 2);
	
	            str += " rx2=" ;
	            str += QString::number(line.p2().rx(), 'f' , 2);
	            str += " ry2=" ;
	            str += QString::number(line.p1().ry(), 'f' , 2);
	
	            this->setToolTip(str);								//这里设置tips字符串
	            this->setToolTipDuration(2000);*
	         }
	         else {
	            *this->setToolTip("");
	            this->setToolTipDuration(2000);*
	         }
	    }
	
	    return line;
}

10)完整头文件如下:

	#ifndef WIDGET_H
	#define WIDGET_H
	
	#include <QWidget>
	#include <QMap>
	#include <QVector>
	#include <QVariant>
	#include <QTimer>
	#include <QLineF>
	#include <QMultiHash>
	#include <QModelIndex>
	#include <QPixmap>
	#include <QImage>
	#include <QRubberBand>
	#include <QLabel>
			
	namespace Ui {
	class Widget;
	}
	
	class Widget : public QWidget
	{
	    Q_OBJECT
	
	public:
	    explicit Widget(QWidget *parent = nullptr);
	    ~Widget();
	public :
	    void calculateRegionRef (qreal Vx, qreal Hx, qreal x1, qreal y1, qreal x2, qreal y2,
	                             qreal &ax1, qreal &ay1, qreal &ax2, qreal &ay2, qreal &ax3, qreal &ay3, qreal &ax4, qreal &ay4);
	
	    void setDataLines(QVector <QLineF> &_lines);                             //设置需要绘制的直线数据
	public slots:
	    void on_drawLines(QVector <QLineF>  _lines);                            //绘制跳频线
	    void timeout();
	
	protected:
	    void paintEvent(QPaintEvent *event) override;
	
	    void mousePressEvent(QMouseEvent *event) override;
	
	    void mouseMoveEvent(QMouseEvent *event) override;
	
	    void mouseReleaseEvent(QMouseEvent *event)override;
	
	    void resizeEvent(QResizeEvent *event);
	
	    //void wheelEvent(QWheelEvent *event);
	private:
	    int  getBlockIndex_Col(qreal   xVal);                                     //判断X值属于的单元格块X向索引
	    int  getBlockIndex_Row(qreal   yVal);                                     //判断Y值属于的单元格块Y向索引
	    void initKeyTable();
	    void positionLines();                                                     //关联直线到单元格,可以通过鼠标点查找到归属单元格,然后在归属单元格中判断击中直线对象,可以加快检索速度
	    void randGerneratLines();                                                 //随机生成直线数据
	    QLineF getObjLineF (QPointF  _pt);
	    bool checkLineHit (const QLineF &_checkline, QPointF _pt);
	    void drawOrgBimp();
	private:
	    Ui::Widget              *ui;
	    QColor                  m_LineColor;
	    QVector <QLineF>        m_vecLines;
	    QTimer*                 m_pTimer;
	
	    //用来记录线对象属于哪一个单元区域的多值hash
	    QMultiHash<QPair<int,int>, QLineF >   m_multiHash;                      //每个单元下会包含的线对象
	
	    //将宽与高均分成10份,也就是一共10 X项hash,10个Y项hash.
	    int                         m_xNumb;                                    //均分等份数目,默认是10
	    int                         m_yNumb;                                    //均分等份数目,默认是10
	    qreal                       m_xCalibrationBase;                         //等于宽/m_xNumb
	    qreal                       m_yCalibrationBase;                         //等于高/m_yNumb
	    QHash <QPair<int,int> , QRectF>     m_keyTableHash;                     //用来存储表格单元矩形的hash容器,其中QPair<int,int>是相当于QModelIndex(row, col)
	    double                      m_xScaleRate;                               //X向缩放比例
	    double                      m_yScaleRate;                               //Y向缩放比例
	
	    QImage*                     m_pImage;                                   //绘制的原始图片
	
	    QRubberBand *               mRubberBand;                                //实现橡皮筋的对象
	    QPoint                      mOrigin;                                    //记录鼠标位置的点对象
	    bool                        m_bPressFlag;                               //鼠标按下标志
	
	    qreal                       m_startX;                                   //用来记录缩放区的真实X起点坐标
	    qreal                       m_startY;                                   //用来记录缩放区的真实Y起点坐标
	
	    QImage                      m_pLocalImage;                              //用来实际显示的图片
	    bool                        m_firstDraw = false;
	
	    bool                        m_bDrawCellLine = true;                     //绘制单元格直线
	    bool                        m_bDrawCellIndex = false;                   //绘制每个单元的索引编号
	};

	#endif // WIDGET_H

有需要样例源码的请到:https://download.youkuaiyun.com/download/wangxuejun1972/21090178
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值