用Qt实现一个桌面弹幕程序(二)--实现一个弹幕②



  • 现在就一起来愉快地编写一个弹幕类吧

    杰洛君 以单身多年的手速很快地 右键 点击项目 新建 了一个Danmu.h 和 Danmu.cpp 文件<( ̄︶ ̄)>

    熟悉C++的你,一定知道接下来就是编写弹幕类的时候了!

    这个弹幕类是重载QLabel的,所以它的类至少应该是这个样子:

    #ifndef DANMU_H
    #define DANMU_H
    
    #include <QLabel>
    
    class Danmu : public QLabel{
    
        Q_OBJECT
    
      public:
    
      Danmu(QWidget * parent);       //构造函数
    
      ~Danmu();     //析构函数
    
      public slots:
    
      private:
    };
    
    #endif // DANMU_H
    

    按照上一篇文章中我们提到了弹幕的好几种属性:

    • 自己的文字内容
    • 自己的字体
    • 自己的颜色
    • 自己的大小
    • 自己的飞行快慢
    • 自己的透明度

    于是在这个类中应该有对应的私有属性:

    • QString 类型的 DText属性
    • QFont 类型的danmuFont属性
    • QColor 类型的qcolor属性
    • int 类型的宽度和高度
    • int 类型的 runTime 持续时间属性
    • double 类型的Transparency属性

    当然,杰洛君后来又想到了一些属性,于是也添加进来:

    • int 类型的 type属性-- --用于区分横飞的弹幕与置顶以及位于底部的弹幕
    • int 类型的PosX与PosY属性-- --用于记录弹幕的初始位置~

    于是乎你的弹幕类中的私有属性至少应该是这样子的:

          int PosX;
          int PosY;
          QString DText;
          QString color;  //这个color属性保存英文颜色字符串
          QColor qcolor;
          int type;
          QFont danmuFont;
          int DHeight;
          double Transparency;
          int runTime;
    

    由于写过一段时间的JavaEE,杰洛君本能地为这些属性写了响应的Get和Set方法<( ̄ˇ ̄)/

    什么是Get和Set方法?
    就是为每个私有成员设置公有的设置和获取方法
    例如:
    void setColor(QString color);
    QString getColor();

    当然你也写成用c++风格的get方法 QString Color();

    小A : 类里的方法都是可以直接访问私有成员的呀,这有什么必要吗?

    杰洛君: 额额,确实可以直接访问,写Get和Set方法是个人编程习惯啦= ̄ω ̄=

    有了这么多私有变量,自然构造函数就要更加复杂啦,但是看到PosX 和 PosY 属性的时候,杰洛君呆住了。。。{{{(>_<)}}} 弹幕的起点怎么确定呀?

    如何确定弹幕的起始位置?

    弹幕是从哪里开始飞的呢?

    小A:简单,从屏幕的最右边开始呗~

    确实,弹幕从屏幕的外部飞入,那么这个具体位置该怎么设置呢,设置一个定值?比如分辨率为800720 那就设置 900吧,这样看起来就是从屏幕外侧飞进来了,那如果是1366768呢?900不就不够了吧。。。

    于是乎获取屏幕的分辨率成为我们要解决的一个问题。

    幸好Qt已经为我们做好了这些工作:

    在Danmu包含的头文件中加入 QRect 与 QDesktopWidget吧~

    QDesktopWidget* desktopWidget;  //获取桌面设备  
    QRect screenRect;
    desktopWidget = QApplication::desktop(); //获取桌面Widget
    screenRect = desktopWidget->screenGeometry(); //获取桌面大小的矩形
    

    这样我们就获得了桌面大小的这样的一个矩形,可以获得它的长和宽,利用这点就可以确定出起始位置的设置方法,保证不同电脑上弹幕的起始位置都在屏幕外。

    于是,杰洛君在Danmu的私有成员中加入了一个QRect 类型的 screenRect属性,这下你知道这个矩形是做什么用的了吧~

    如何确定弹幕的高度和宽度?

    弹幕只有一行内容所以它的高度是单个字的高度,这个几乎可以说是固定的,但是它的长度就不是固定的了,随用户输入字数多少而变化。

    这时如果能够根据内容的多少知道我们的字体会有多长该有多好呀~

    好在Qt 也为我们做好这个工作

    利用QFontMetrics类中的 int QFontMetrics::width(const QString & text, int len = -1) const方法 可以获得一段字符串的像素长度。

    具体用法:

    QFontMetrics metrics(this->getQFont()); //传入一个QFont字体
    metrics.height(); //得到字体高度
    metrics.width(DText); //获得DText字符串的像素宽度
    this->setFixedHeight(metrics.height()+5); //弹幕设置固定的高度
    this->setFixedWidth(metrics.width(DText)+4); //弹幕设置固定的宽度

    如果你觉得不保险,可以在这基础上再加上一些整数,因为背景是透明的,所以只要字体显示完整就可以了

    如何绘制弹幕内容?

    上篇文章中我们用到了 QPalette 改变弹幕的颜色,通过改变字体去改变弹幕的大小。

    但是这个效果其实并不好,没有描边,看起来就是不像弹幕。

    于是,杰洛君决定重载 弹幕的 绘制函数 重绘出我们需要的效果来。

    首先在 Danmu类中添加 :

    protected:
          void paintEvent(QPaintEvent *);       //弹幕的绘制函数
    

    这个是重载了QLabel的绘制函数,在接收到绘制时间信号时会调用这个方法。

    下面是它的实现:

    void Danmu::paintEvent(QPaintEvent *){  //弹幕绘制函数
            QPainter painter(this);     //以弹幕窗口为画布
            painter.save();
            QFontMetrics metrics(this->getQFont());     //获取弹幕字体
            QPainterPath path;      //描绘路径用
            QPen pen(QColor(0, 0, 0, 230));       //自定义画笔的样式,让文字周围有边框
            painter.setRenderHint(QPainter::Antialiasing);  //反走样
            int penwidth = 4;  //设置描边宽度
            pen.setWidth(penwidth);
            int len = metrics.width(DText);
            int w = this->width();
            int px = (len - w) / 2;
            if(px < 0)
            {
                px = -px;
            }
            int py = (height() - metrics.height()) / 2 + metrics.ascent();
            if(py < 0)
            {
                py = -py;
            }
            path.addText(px+2,py+2,this->getQFont(),DText);     //画字体轮廓
            painter.strokePath(path, pen);  //描边
            painter.drawPath(path);  //画路径
            painter.fillPath(path, QBrush(this->getQColor()));      //用画刷填充
            painter.restore();
    }
    

    这里用到了QPainter类,这个类很重要,建议多看看文档,熟悉熟悉它的用法。有机会杰洛君会找几个例子好好描述它。

    这里比较难理解的是py的计算,里面用到了 metrics.ascent(),这个是什么呢?杰洛君通过F1的帮忙知道了这个是什么。

    int QFontMetrics::ascent() const
    The ascent of a font is the distance from the baseline to the highest position characters extend to.

    ascent 就是字体的最高位置到字体基线这么一段距离。

    所以py的计算就是 弹幕窗体的高度 - 字体的高度 得到留白的大小,这个留白大小平均分之后 再加上 基线以上的字体高度,得到真正绘制字体的位置。

    小C:你这么说,我怎么懂呀。。。

    确实用杰洛君拙劣的文字描述很难懂,怎么办呢?下面就看看文档的图示吧~

    0_1456660335692_pic13.png

    从这张图中我们可以清晰地看到基线并不是文字的最下方。这么看是不是好懂些了。

    如何让弹幕飞行?

    好了,杰洛君知道屏幕左上角为(0,0)位置,利用上述的屏幕矩形,把弹幕的起始横向位置设置为 矩形宽度加上 200 或者 随机某个整数,这样弹幕的横向位置就看起来比较随机了。

    而纵向设置为 屏幕宽度 以内的随机整数,这样竖直方向的弹幕看起来也就比较随机了。

    (p.s.其实范围应该是屏幕高度减去字体高度,小伙伴们可以想一想这是为什么?)

    如何生成随机数?
    就像c++中有srand() 和 rand()函数一样,Qt 有 qsrand() 和 qrand() 函数
    具体用法:
    头文件加上QTime
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
    int y = qrand()%rect.height(); //得到矩形高度范围内的一个随机数

    问题来了,弹幕如何才能做到飞行?

    方案 1:

    使用Qt 的 QTimer定时器,每隔20ms 调用一个槽函数
    这个槽函数做弹幕X方向坐标递减,然后弹幕 move到相应的位置
    重绘
    判断是否飞离屏幕,若飞离则析构弹幕。

    对于这个方案,确实可行,效果也不错,但是有一个问题就是,弹幕一旦多起来就会看起来很卡。

    (不要问杰洛君是怎么知道的ヽ(≧□≦)ノ)

    方案 2:

    弹幕移动就是一个动画,那就用Qt 的QPropertyAnimation吧~
    确定起始位置,终点位置和持续时间
    启动动画
    动画结束时就析构弹幕

    恩恩,方案二看起来更好呀,那就用它吧~(^__^)

    QPropertyAnimation * anim2=new QPropertyAnimation(this, "pos");
    anim2->setStartValue(QPoint(this->getPosX(),this->getPosY()));
    anim2->setEndValue(QPoint(-(this->width()), this->getPosY()));
    anim2->setDuration(this->getRunTime());
    anim2->setEasingCurve(QEasingCurve::Linear);
    this->setWindowOpacity(this->getTransparency());
    this->show();
    anim2->start();
           
    //利用Qt的信号和槽函数响应机制,让动画销毁时 弹幕 析构       
    
    connect(anim2,SIGNAL(finished()),this,SLOT(deleteLater()));
    

    上面这段代码应该放在哪里呢?杰洛君把它放在了Danmu的构造函数中。

    或许你会觉得anim2这个指针只有new 没有 delete 存在内存泄漏。

    不过Qt 拥有一个机制,那就是如果该对象为QObject的派生类,销毁父对象时会销毁它的子对象,而我们的动画对象是有声明父对象的,所以析构弹幕时动画也会销毁。

    千辛万苦终于走到这步

    好了,细节设计好了,就设计一下构造函数吧~

    Danmu(
    QWidget * parent,
    QString text,
    QString color,
    int type,
    QRect rect,
    QFont danmuFont = QFont("SimHei",20,100),
    double Transparency = 1.00,
    int runTime=15000
    );       //构造函数,常用
    

    在构造函数中,带有默认参数,这样可以减轻很多工作量。

    ###具体Danmu.h头文件实现:

    #ifndef DANMU_H
    #define DANMU_H
    
    #include <QLabel>
    #include <QRect>
    #include <QColor>
    #include <QDebug>
    #include <QTextCharFormat>
    #include <QPainter>
    #include <iostream>
    #include <QTime>
    #include <QPropertyAnimation>
    #include <QParallelAnimationGroup>
    class Danmu : public QLabel{
    
        Q_OBJECT
    
      public:
    
          Danmu(QWidget * parent,QString text,QString color,int type,QRect rect,QFont danmuFont = QFont("SimHei",20,100),double Transparency = 1.00,int runTime=15000);       //构造函数,常用
    
          ~Danmu();     //析构函数
    
          //一些成员变量的Get方法与Set方法
          //全部放上来会显得累赘所以略去
    
      protected:
          void paintEvent(QPaintEvent *);       //重点,弹幕的绘制函数
    
      private:
          int PosX;
          int PosY;
          QString DText;
          QString color;
          QColor qcolor;
          int type;
          QFont danmuFont;
          int DHeight;
          double Transparency;
          QRect screenrect;
          QPropertyAnimation *anim2;
          int runTime;
    };
    
    #endif // DANMU_H
    

    ###具体Danmu.cpp文件实现:

    #include "Danmu.h"
    
    Danmu::Danmu(QWidget * parent,QString text,QString color,int type,QRect rect,QFont danmuFont,double Transparency,int runTime):QLabel(parent){
        DText = text;
        //this->setText(text);        //设置内容
        this->setColor(color);      //设置内容
        this->setType(type);        //设置类型
        this->setQFont(danmuFont);      //弹幕字体
        this->setTransparency(Transparency);        //弹幕透明度
        this->setRunTime(runTime);
        this->setScreenRect(rect);
        QFontMetrics metrics(this->getQFont());
        QPalette palll=QPalette();
        QString DColor = this->getColor();
        anim2 = NULL;
        //颜色字符串转化为特定的颜色
        if(DColor == "White"){
            palll.setColor(QPalette::WindowText,QColor(255,255,246,255));
            this->setQColor(QColor(255,255,246,255));
        }else if(DColor =="Red"){
            palll.setColor(QPalette::WindowText,QColor(231,0,18,255));
            this->setQColor(QColor(231,0,18,255));
        }else if(DColor =="Yellow"){
            palll.setColor(QPalette::WindowText,QColor(254,241,2,255));
            this->setQColor(QColor(254,241,2,255));
        }else if(DColor =="Brown"){
            palll.setColor(QPalette::WindowText,QColor(149,119,57,255));
            this->setQColor(QColor(149,119,57,255));
        }else{
            palll.setColor(QPalette::WindowText,QColor(255,255,246,255));
            this->setQColor(QColor(255,255,246,255));
        }
        this->setPalette(palll);        //设置调色盘
        this->setFixedHeight(metrics.height()+5);
        this->setFixedWidth(metrics.width(DText)+4);
        int yy = qrand()%rect.height(); //临时的位置,还要防止高度在屏幕下导致显示不完整
        int y = yy<(rect.height()-metrics.height()-5)?(yy):(rect.height()-metrics.height()-5);
        int xx = rect.width()+qrand()%500;
        this->move(xx,y);
        this->setPosX(xx);//设置弹幕水平的位置
        this->setPosY(y);       //设置弹幕垂直位置
    
        this->setWindowFlags(Qt::FramelessWindowHint|Qt::Tool|Qt::WindowStaysOnTopHint);    //设置弹幕为无窗口无工具栏且呆在窗口顶端
    
        this->setAttribute(Qt::WA_TranslucentBackground, true);
        this->setFocusPolicy(Qt::NoFocus);
        this->hide();
        anim2=new QPropertyAnimation(this, "pos");
        anim2->setDuration(this->getRunTime());
        anim2->setStartValue(QPoint(this->getPosX(),this->getPosY()));
        anim2->setEndValue(QPoint(-(this->width()), this->getPosY()));
        anim2->setEasingCurve(QEasingCurve::Linear);
        this->setWindowOpacity(this->getTransparency());
        this->show();
        this->repaint();
        anim2->start();
            //connect(anim2,SIGNAL(finished()),this,SLOT(deleteLater()));
    }
    
    void Danmu::paintEvent(QPaintEvent *){  //弹幕绘制函数
            QPainter painter(this);     //以弹幕窗口为画布
            painter.save();
            QFontMetrics metrics(this->getQFont());     //获取弹幕字体
            QPainterPath path;      //描绘路径用
            QPen pen(QColor(0, 0, 0, 230));       //自定义画笔的样式,让文字周围有边框
            painter.setRenderHint(QPainter::Antialiasing);
            int penwidth = 4;
            pen.setWidth(penwidth);
            int len = metrics.width(DText);
            int w = this->width();
            int px = (len - w) / 2;
            if(px < 0)
            {
                px = -px;
            }
            int py = (height() - metrics.height()) / 2 + metrics.ascent();
            if(py < 0)
            {
                py = -py;
            }
            path.addText(px+2,py+2,this->getQFont(),DText);     //画字体轮廓
            painter.strokePath(path, pen);
            painter.drawPath(path);
            painter.fillPath(path, QBrush(this->getQColor()));      //用画刷填充
            painter.restore();
    }
    
    
    
    Danmu::~Danmu(){
        qDebug()<<"弹幕被析构"<<endl;
    }
    
    //下面是get和set方法的实现
    //同样全部放上无法凸显重点故略去
    
    
    

    上面就是具体代码了,这段代码有很多可以优化的地方。

    其中那一大段if else 想必会被很多人唾弃吧。

    不过还请体谅体谅杰洛君,这个可怜的娃为了实现这么小一个功能已经耗尽脑筋,放弃思考了〒▽〒。

    有时间会采用enum与switch-case做替换的。

    至于颜色的取值,大家可以找一些简单的取色软件获取颜色的16位值或者RGB值

    关于最终程序中用到的颜色,这里给个参考:

    • "White" QColor(255,255,246,255));
    • "Red" QColor(231,0,18,255));
    • "Yellow" QColor(254,241,2,255));
    • "Green" QColor(0,152,67,255));
    • "Blue" QColor(0,160,234,255));
    • "Pink" QColor(226,2,127,255));
    • "Grass" QColor(144,195,32,255));
    • "DBlue" QColor(0,46,114,255));
    • "DYellow" QColor(240,171,42,255));
    • "DPurple" QColor(104,58,123,255));
    • "LBlue" QColor(129,193,205,255));
    • "Brown" QColor(149,119,57,255));

    这些颜色的值都取自B站手机客户端上的可选颜色~

    在main函数中试一试吧

    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
        QDesktopWidget* desktopWidget;                                    //获取桌面设备
        QRect screenRect;
        desktopWidget = QApplication::desktop();                   //获取桌面设备
        screenRect = desktopWidget->screenGeometry();              //获取桌面大小的矩形
        Danmu danmu(NULL,"Hello World","Red",1,screenRect);
        w.show();
    
        return a.exec();
    }
    

    不出意外你就会看到一个弹幕飞过了,啊啊啊,终于成功了!!!ค(TㅅT)

    杰洛君已经哭成狗狗了。。。

    你会看到那个动画那段connect代码被注释了。

    这是因为启用这段代码,动画结束后就会析构弹幕,但是我们的弹幕不是在堆上新建的对象,所以delete一个栈上的对象 退出程序时,这个弹幕的析构函数将被再次调用,一个对象调用了两次析构函数,程序当然就瞬间爆炸了。

    所以注释了这段代码,这个不是大问题,只用new的方式建立Danmu对象就可以启用这段代码了。

    后续

    实现了一个弹幕了,但是后续问题接踵而至。

    弹幕失去控制了!!!

    我想隐藏它该怎么办?

    弹幕好污好羞耻又该如何监控。

    放心,后续都会讲到的,今天就到这里吧~O(∩_∩)O哈哈~

    或许你会觉得杰洛君废话很多,不过这些废话姑且算是自己的思考过程进去,希望你们会喜欢~。

    题外话
    本来博文早上应该就可以发的了,不过我认识的一位会画画的女神大大上午指导我画画去了~

    0_1456661850900_draw.jpg

    我只能表示:

    手残党上女神的课一上午,胜过画室苦画三年呀!



  • @杰洛飞 说:

    弹幕是从哪里开始飞的呢?

    小A:简单,从屏幕的最左边开始呗~

    不对啊。应该从屏幕的最右边飞啊。



  • 最后这个线稿画得很棒!



  • @jiangcaiyang 对对对,我改改



  • @jiangcaiyang 谢谢☺ 会继续把它练好~



  • 啪啪啪,写的不错。弹幕解析那一块,能不能直接解析b站的弹幕文件?最后线稿不错。



  • @qyvlik 这个没有做哦,不过可以作为这个程序的一个拓展方向,有机会会试试,嘿嘿😝



  • @杰洛飞 好好好。



  • 最后的手绘是本文一大亮点 -。-



  • 不是很明白,什么叫做“弹幕类”?它的目的是什么?



  • @stlcours 弹幕就是 类似 文字跑马灯效果,也就是通常情况下让文字从右往左进行移动。这个效果呢,实现起来很简单,也可以用Qt Quick以及Qt Widgets实现。



  • 用Label性能会比较悲催
    建议从qtquick的Text做起可能比较好



  • @MidoriYakumo

    嘿嘿嘿, DarkFlameMaster 这个是用 QML 实现的弹幕播放器,不过在弹幕轨迹分配和弹幕播放器中的时间轴算法找不到其他可以参考的。有什么好的相关文章推荐吗?



  • 没研究过诶,你那个有什么问题么?


 

走马观花

最近的回复

关注我们

微博
QQ群











召唤伊斯特瓦尔