杰洛君 以单身多年的手速很快地 右键 点击项目 新建 了一个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
按照上一篇文章中我们提到了弹幕的好几种属性:
文字内容
字体
颜色
大小
飞行快慢
透明度
于是在这个类中应该有对应的私有属性:
当然,杰洛君后来又想到了一些属性,于是也添加进来:
于是乎你的弹幕类中的私有属性至少应该是这样子的:
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:你这么说,我怎么懂呀。。。
确实用杰洛君拙劣的文字描述很难懂,怎么办呢?下面就看看文档的图示吧~
从这张图中我们可以清晰地看到基线并不是文字的最下方。这么看是不是好懂些了。
好了,杰洛君知道屏幕左上角为(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
); //构造函数,常用
在构造函数中,带有默认参数,这样可以减轻很多工作量。
#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
#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文档,所以加了些自己的思考过程进去,希望你们会喜欢~。