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

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

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

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

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

#ifndef DANMU_H
#define DANMU_H

#include 

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();

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

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

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

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

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

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

确实,弹幕从屏幕的外部飞入,那么这个具体位置该怎么设置呢,设置一个定值?比如分辨率为800*720 那就设置 900吧,这样看起来就是从屏幕外侧飞进来了,那如果是1366*768呢?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:你这么说,我怎么懂呀。。。

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

用Qt实现一个桌面弹幕程序(三)--实现一个弹幕②_第1张图片

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

如何让弹幕飞行?

好了,杰洛君知道屏幕左上角为(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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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方法
      int getPosX();

      int getPosY();

      void setPosX(int posx);

      void setPosY(int posy);

      void setColor(QString color);

      QString getColor();

      void setType(int type);

      int getType();

      void setQColor(QColor qcolor);

      QColor getQColor();

      void setQFont(QFont danmuFont);

      QFont getQFont();

      void setTransparency(double Transparency);

      double getTransparency();

      void setScreenRect(QRect screenRect);

      QRect getScreenRect();

      int getRunTime();

      void setRunTime(int runTime);

      QPropertyAnimation * getanimation();

  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 == "Green"){
        palll.setColor(QPalette::WindowText,QColor(0,152,67,255));
        this->setQColor(QColor(0,152,67,255));
    }else if(DColor == "Blue"){
        palll.setColor(QPalette::WindowText,QColor(0,160,234,255));
        this->setQColor(QColor(0,160,234,255));
    }else if(DColor == "Pink"){
        palll.setColor(QPalette::WindowText,QColor(226,2,127,255));
        this->setQColor(QColor(226,2,127,255));
    }else if(DColor == "Grass"){
        palll.setColor(QPalette::WindowText,QColor(144,195,32,255));
        this->setQColor(QColor(144,195,32,255));
    }else if(DColor == "DBlue"){
        palll.setColor(QPalette::WindowText,QColor(0,46,114,255));
        this->setQColor(QColor(0,46,114,255));
    }else if(DColor == "DYellow"){
        palll.setColor(QPalette::WindowText,QColor(240,171,42,255));
        this->setQColor(QColor(240,171,42,255));
    }else if(DColor =="DPurple"){
        palll.setColor(QPalette::WindowText,QColor(104,58,123,255));
        this->setQColor(QColor(104,58,123,255));
    }else if(DColor == "LBlue"){
        palll.setColor(QPalette::WindowText,QColor(129,193,205,255));
        this->setQColor(QColor(129,193,205,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();
}

void Danmu::setScreenRect(QRect screenRect){
    this->screenrect = screenRect;
}

QRect Danmu::getScreenRect(){
    return this->screenrect;
}

Danmu::~Danmu(){
    qDebug()<<"弹幕被析构"<int Danmu::getPosX(){
    return PosX;
}

int Danmu::getPosY(){
    return PosY;
}

void Danmu::setPosX(int posx){
    this->PosX = posx;
}

void Danmu::setPosY(int posy){
    this->PosY = posy;
}

QString Danmu::getColor(){
    return color;
}

int Danmu::getType(){
    return type;
}

void Danmu::setColor(QString color){
    this->color = color;
}

void Danmu::setType(int type){
    this->type = type;
}

QColor Danmu::getQColor(){
    return qcolor;
}

void Danmu::setQColor(QColor qcolor){
    this->qcolor = qcolor;
}

QFont Danmu::getQFont(){
    return danmuFont;
}

void Danmu::setQFont(QFont danmuFont){
    this->danmuFont = danmuFont;
}

void Danmu::setTransparency(double Transparency){
    this->Transparency = Transparency;
}

double Danmu::getTransparency(){
    return Transparency;
}

void Danmu::setRunTime(int runTime){
    this->runTime = runTime;
}

int Danmu::getRunTime(){
    return this->runTime;
}

QPropertyAnimation * Danmu::getanimation(){
    return anim2;
}

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

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

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

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

在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一个栈上的对象 就会 瞬间爆炸! 程序崩溃!

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

后续

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

弹幕失去控制了!!!

我想隐藏它该怎么办?

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

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

或许你会觉得杰洛君废话很多,不过我真的不希望自己的博文变成翻译API文档,所以加了些自己的思考过程进去,希望你们会喜欢~。

你可能感兴趣的:(Qt,qt)