1.4.1任务描述
“媒体播放器”软件概要设计的基本任务在软件需求分析阶段,已经搞清楚了软件“做什么”的问题,并把这些需求通过规格说明书描述了出来,这也是目标系统的逻辑模型。进入了设计阶段,要把软件“做什么”的逻辑模型变换为“怎么做”的物理模型,即着手实现软件的需求,并将设计的结果反映在“设计规格说明书”文档中,所以软件设计是一个把软件需求转换为软件表示的过程,最初这种表示只是描述了软件的总的体系结构,称为软件概要设计或结构设计,是软件设计人员做详细设计的依据。
1.4.2任务目的
本任务的目的旨在推动软件的规范化,使软件设计人员遵循统一的书写规范,节省制作文档的时间,降低系统应用开发的风险,做到软件资料的规范性与全面性,以利于系统的实现、测试、维护、版本升级等。在需求分析上做系统设计,最终形成媒体播放器的系统设计规格说明书,提高学生团队合作精神和分析问题、解决问题的能力。
1.4.3知识准备
1、什么是系统设计
进入了设计阶段,要把软件“做什么”的逻辑模型变换为“怎么做”的物理模型,即着手实现软件的需求,并将设计的结果反映在“设计规格说明书”文档中,所以软件设计是一个把软件需求转换为软件表示的过程,最初这种表示只是描述了软件的总的体系结构等,称为软件概要设计或结构设计。显然系统设计建立的是目标系统的逻辑模型,与计算机无关。
2、系统设计的基本任务
(1)设计软件系统结构(简称软件结构)为了实现目标系统,最终必须设计出组成这个系统的所有程序和数据库(文件),对于程序,则首先进行结构设计,具体为:
①采用某种设计方法,将一个复杂的系统按功能划分成模块。
②确定每个模块的功能。
③确定模块之间的调用关系。
④确定模块之间的接口,即模块之间传递的信息。
⑤评价模块结构的质量。
(2)数据结构及数据库设计
①数据结构的设计在需求分析阶段,已通过数据字典对数据的组成、操作约束、数据之间的关系等方面进行了描述,确定了数据的结构特性,在概要设计阶段要加以细化,详细设计阶段则规定具体的实现细节。在概要设计阶段,宜使用抽象的数据类型。
②数据库的设计
数据库的设计指数据存储文件的设计,主要进行以下几方面设计:概念设计、逻辑设计、物理设计。对于不同的DBMS,物理环境不同,提供的存储结构与存取方法各不相同。物理设计就是设计数据模式的一些物理细节,如数据项存储要求、存取方式、索引的建立。
3、Visio
Microsoft Office Visio 2003 是微软公司出品的一款的软件,它有助于 IT 和商务专业人员轻松地可视化、分析和交流复杂信息。它能够将难以理解的复杂文本和表格转换为一目了然的 Visio 图表。该软件通过创建与数据相关的 Visio 图表(而不使用静态图片)来显示数据,这些图表易于刷新,并能够显著提高生产率。使用 Office Visio 2003 中的各种图表可了解、操作和共享企业内组织系统、资源和流程的有关信息。
1.4.4任务实现
2、总体结构设计
总体结构示意图如1—13所示。

图1-13 总体结构示意图
其中,基本操作是在本地上进行的,而其他操作是需要和服务器端的数据库联系的。所以在服务器端,需要对客户端发过来的服务请求进行分别处理,而它们主要是以函数的形式完成的。
4、开发环境的配置
如表1—3所示
提示:说明本系统应当在什么样的环境下开发,有什么强制要求和建议?
| 类别 | 标准配置 | 最低配置 |
| 计算机硬件 | PC机 | 略 |
| 软件 | Windows、VC++ | 略 |
| 网络通信 | 无需 | 略 |
| 其它 |
表1—3
对于模块的具体设计,我这里是根据图2.1给出的功能的顺寻进行介绍的。
3.1数据库设计
数据库使用的是MySQL,数据库的使用是在服务器端进行的,服务器端程序的编写也是在QT4环境下完成的,为了能够使用数据库,要首先在QT4中对MySQL进行编译,得到MySQL的插件,成功后就可以用了。还有就是大部分歌曲名、歌手等使用的是汉字,所以在QT4中要有包含GBK,在MySQL中也要包含GBK(可以在安装时选择GBK,然后再查找资料,对安装目录中的指定文件进行修改)。
QT4中使用汉字代码:
QApplication::addLibraryPath("./plugins"); QTextCodec::setCodecForLocale(QTextCodec::codecForName("GB2312"));
QTextCodec::setCodecForTr(QTextCodec::codecForName("GB2312"));
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("GB2312"));
其中第一句是针对于程序发布时显示汉字用的,当程序发布时,需要将程序用到的各种DLL文件复制到发布文件夹下,而且如果想显示汉字,还得将qcncodecs4.dll文件放在“./plugins”中,如果想显示图片、图标,就要有QT中imageformats文件夹中的全部内容。
数据库的名称为mymusic,它其中的表有user、song等,如下图,可以看出,除了用户表和歌曲表外,还有四个其他的表,他们是用户自己的表,用来记录各自喜欢的歌曲id信息。

图3.1 mymusic的表
3.1.1用户表
用户表是user,用来存储用户名、密码、用户昵称,用户名用来登录和用户识别,它是user表的主键,密码是用来登录的,用户昵称显示在客户端的标题栏,使程序更加亲切。
下图是user表的结构:

图3.2 user表的结构
3.1.2歌曲表
歌曲表是song表,它里面存储了搜集来的歌曲的相关信息,包括歌曲id、歌曲URL地址、歌曲名、歌手、歌曲类型、歌词URL地址,其中歌曲id是主键。当用户向服务器端发送歌曲请求时,服务器端就会返回歌曲URL地址、歌曲名、歌手、歌词地址等信息,然后客户端就会根据这些地址、信息进行下载、显示等相应的操作。
下图是song表的结构:

图3.3 song表的结构
3.1.3用户自身的表
程序对每个用户创建了一个表名为用户名的表,当用户注册时会自动创建,该表存放的是用户喜欢歌曲相对于song表中的id值,也就是说存放的是用户喜欢的歌曲信息。
下图是用户自身的表的结构:

图3.4 用户自身表的结构
3.2基本操作
基本操作是基于QT自带的Phonon类实现的,Phonon类中定义了基本操作函数、信号、状态等,而且它读取网络音频使用的是流媒体技术,很适合本程序的实现。
在本程序中,本地存有两个与基本操作相关的XML文件,其中之一是usersong.xml,它其中存的是用户喜欢的歌曲列表的相关信息,包括歌曲在服务器数据库中的id号、歌曲URL地址、歌曲名、歌手、歌词URL地址,当用户选中一首歌曲为喜欢时,将歌曲的相关信息写入此xml文件,当用户对于已经选为喜欢的歌曲标记为不喜欢时,在此xml文件中删除该歌曲的相关信息,对于本地的歌曲则不存储。

图3.6 username.xml的结构
另外一个xml文件是songpos.xml,它里面存储的是用户退出程序时最后播放的用户喜欢的音乐,也就是usersong.xml里面的一首歌曲的位置,从一开始,每当程序启动时,读取songpos.xml文件得到上次最后的喜欢歌曲,重新播放。

图3.7 songpos.xml的结构
当用户退出程序时,在主窗口类的析构函数中有写songpos的操作,流程和一般的写XML的流程一样,加头、创建节点、创建文本、添加字结构等。
由于在很多函数里面要用到对usersong.xml文件的读和写,所以在程序中将对此文件的读和写定义了函数,其中readinformation(intpos)是读取在usersong.xml中位置为pos的歌曲的信息,并将歌曲信息赋值给全局的结构体current,其中current的结构和username.xml文件里面存放的歌曲结构相同;writeinformation()的功能是将当前的音乐信息current存储到usersong.xml文件中的全局变量songpos+1的位置,这个只是在用户选择当前歌曲为喜欢时才会调用;deleteinformation()的功能是在usersong.xml文件中删除当前位置的歌曲信息,也就是当用户选择以前喜欢的歌曲为不喜欢时,为了保持一致性,将删除这条歌曲的信息。
在readinformation函数中,需要首先判断songpos的特殊位置,然后循环到应该读取的节点,之后读取节点中的数据并赋值给current,其中还要判断当前的歌词是否有文件,是否文件名和歌词URL中给出的相同等。
writeinformation函数的实现比较麻烦,因为XML没有插入的功能,所以只能分四步进行:1.打开原文件并读取,创建新根节点,加头部信息,创建新的节点数组等;2.读取先前文件中的songpos位置之前的节点数据并添加给新的根节点;3.添加current中的数据到根节点下;4.读取并添加先前文件中的剩余节点信息到根节点,将根节点连接到文件变量中并写入usersong.xml中。至此完成了改函数。
Deleteinformation函数的实现比较简单,先找到指定位置的节点,然后使用root.removeChild(n)函数即可。
3.2.1上一首
上一首按钮连接了preFile()函数,它的功能是通过prevFile()函数来实现的。在此函数中,有一个特殊情况,就是当当前正是电台播放时,此功能应该是无效的,所以需要判定。对于一般的情况,则读取usersong.xml文件中位置为songpos-1的歌曲信息,赋值给current,然后进行播放,实现上一首功能。
3.2.2播放
此功能比较简单,当点击播放按钮时,隐藏播放按钮,显示暂停按钮,并读取current中的信息,通过Phonon的函数play()实现播放。
3.2.3暂停
此功能也比较简单,当点击暂停按钮时,隐藏暂停按钮,显示播放按钮,调用Phonon中的pause()函数,将播放暂停。
3.2.4停止
当点击停止按钮时,隐藏暂停按钮,让停止按钮变为不可用,调用Phonon的stop()函数,将音乐停止。
3.2.5下一首
当点击下一首按钮时,调用nextFile()函数,并且当歌曲播放完之后会产生一个aboutToFinish()信号,这个时候也会调用nextFile()函数。在此函数中先判断当前是否为模拟电台的播放状态,若是,则发送信息给服务器端,得到下一首的电台播放的歌曲信息,赋值给current,并播放;若不是,则调用readinformation()函数,得到下一首的信息,并播放。
3.2.6打开本地文件
点击打开本地文件会跳出获取本地文件路径和名称的对话框,当用户选中后,因为它是本地文件,会直接把地址赋给播放的对象,而不会经过current结构体,songpos也不会改变,而且由于本地文件不是服务器端提供的,所以喜欢和不喜欢功能不能使用,当播放完后继续播放usersong.xml中的下一首,
下图是点击打开本地文件后的显示情况:
3.2.7音量、进度调节
音量对应的控件是Phonon::VolumSlider,ui->volumeSlider->setAudioOutput(audiooutput),此语句将音量的控件连接到了音频输出的设备上,而此音频输出的设备是通过语句Phonon::createPath(mediaobject,audiooutput)和播放的对象mediaobject连接上的,这样就可以通过调节音量控件而控制播放的音量。
播放进度的控制是通过Phonon::SeekSlider控件实现的,通过以下的这条语句:ui->seekSlider->setMediaObject(mediaobject),建立了进度条和播放对象的连接,实现了拖放功能。
3.2.8时间显示
connect(mediaobject,SIGNAL(tick(qint64)),this,SLOT(tick(qint64))),此语句将播放对象的时间变化对应上了函数tick(qint64),而当前播放的时间会传给tick函数。时间显示的控件是QLCDNumber类,在函数中,将播放的当前时间进行转化,再通过控件的display()方法就可以实现时间的显示。
![]() |
图3.9 时间显示
上图中的时间栏的效果是使用一下语句完成的:
QPalettepalette;
palette.setBrush(QPalette::Light,Qt::darkGray);
ui->lcdNumber->setPalette(palette);
3.3其他操作
3.3.1注册
为了实现对不同用户分别对待,此播放器需要用户登录,而新用户则可以注册,注册后服务器端会在表user中记录下用户的用户名、密码和昵称,同时会新建一个表名和用户名一样的表,用于存储用户在使用过程中的喜欢歌曲。
下图是用户注册的窗口:

图3.10 注册窗口
在上图中,先进入的是登录窗口,如果用户没有账号,则选择注册,进入注册窗口,填上用户名、密码、重复密码、昵称,点击确定就可以注册了,同时发送消息给服务器,如果该用户名没有使用,则显示注册成功并在数据库的user中添加该用户信息,并且创建一个属于用户的表;如果用户名已经有人用了,如上图,则注册不成功。
在下图中可以看到该用户的存在:

图3.11 用户的存在显示
注测功能的过程是:
1.客户端向服务器端发送信息:
| 块大小 | 服务类型’R’ | 用户名 | 密码 | 昵称 |
2.服务器端接受该消息后,调用registersend(QStringusername,QStringpassword,QStringnickname)函数,先在user表里查询该用户名是否已经存在,如存在则返回给客户端布尔型值false;如果不存在则使用"insertintouser values(:username,:password,:nickname)"和"createtable"+username+"(IDint(11))"在user表中添加用户信息,并建立一个用户自己的表,返回给客户端true。
3.客户端接受到bool型值后就知道了是否注册成功,至此注册模块完成。
3.3.2登录
为了方便用户,每当用户登录成功后会将用户的用户名和昵称记录在本地的user.xml文件中,下一次登录时,会自动读取此文件,提取用户名显示出来。

图3.12 user.xml的结构
下图是显示的上次登录成功后本次在打开程序时的情况,可以看出自动读取了user.xml文件:

图3.13 登录窗口
登录功能的过程是:
| 块大小 | 服务类型’L’ | 用户名 | 密码 |
1.客户端发送消息:
2.服务器端接受到服务类型后调用loginsend(QStringusername,QStringpassword)函数,使用"select*fromuserwhere(username=:usernameandpassword=:password)"进行查询,看该用户是否正确登录,如是,则返回bool值true以及用户昵称;若不是,则返回false。
3.客户端得到信息后确定能否登录到主界面。
3.3.3喜欢
用户根据自己的喜好对音乐进行分类这个过程是通过用户使用喜欢和不喜欢功能来实现的,默认的是不喜欢,当播放的是本地文件时,喜欢和不喜欢都为不可操作状态,当播放的是服务器端数据库中的歌曲时,若此歌曲已经标记为喜欢过了,则显示不喜欢按钮,若没有标记为喜欢,则显示喜欢按钮。当用户对当前歌曲点击喜欢按钮时,程序会通知服务器端,服务器端再将此歌曲在歌曲库song表中的id号存入表名为用户名的表中,表示用户喜欢此歌曲,同时,在本地则调用writeinformation()函数,将此歌曲记录在usersong.xml的当前位置的下一个位置。
下图是liuzhilong用户已经选为喜欢的歌曲名称:

图3.14 用户选为喜欢的歌曲
喜欢功能的过程是:
1.用户点击“喜欢”按钮,则发送数据到服务器端:
| 块大小 | 服务类型’O’ | 用户名 | true | 歌曲id |
其中,喜欢和不喜欢同样都用服务类型’O’,所以后面要接一个bool型值,true代表的是喜欢,歌曲id表示的是针对的歌曲对象。
2.服务器端接收到信息后,调用likeornotsend(QStringusername,intid,boollike)函数,如果like=true,则表示喜欢,"insertinto"+username+"values("+tr("%1").arg(id)+")",将该歌曲的id号插入到对应用户的表中,完成了喜欢功能。
3.3.4不喜欢
当播放的是服务器端song表中的歌曲,并且用户已经将该歌曲标记为了喜欢,则客户端的程序显示不喜欢按钮,隐藏喜欢按钮,当用户觉得这首歌不好听,点击不喜欢按钮时,一方面发送消息到服务器端,服务器端在该用户的表中删除此歌曲的id;令一方面,在本地的usersong.xml文件中调用deleteinformation()函数,删除当前的记录,歌曲按照当前的状态播放。
不喜欢功能的过程是:
1.用户点击“不喜欢”按钮,则发送数据到服务器端:
| 块大小 | 服务类型’O’ | 用户名 | false | 歌曲id |
其中,喜欢和不喜欢同样都用服务类型’O’,所以后面要接一个bool型值,false代表的是不喜欢,歌曲id表示的是针对的歌曲对象。
2.服务器端接收到信息后,调用likeornotsend(QStringusername,intid,boollike)函数,如果like=false,则表示不喜欢,"deletefrom"+username+"whereID="+tr("%1").arg(id),将该歌曲的id号从对应用户的表中删除,完成了不喜欢喜欢功能。
3.3.5搜索歌曲
当点击搜索歌曲按钮时,跳出一个对话框,当输入了歌曲名的一部分并确定后,则发送消息到服务器端,服务器端对表song进行查询,得到符合的歌曲列表,然后对搜索结果中的每一首歌曲在该用户的表中进行查询,看词歌曲是否是用户已经选为喜欢的,在将这些信息返回给客户端(包括每首歌曲用户是否已选为喜欢的信息),在客户端形成一个QwidgetTable控件,用来显示搜索的结果,每一首个显示歌曲名和歌手,而传过来的包括前面提到的歌曲的五个属性。当用户点击其中的一行时,将相应行的歌曲信息赋值给current,并播放,同时,根据此歌曲是否已选为喜欢决定喜欢按钮和不喜欢按钮的显示状态。
搜索歌曲功能的实现过程:
1.用户点击搜索按钮,新建一个搜索歌曲对话框,当用户输入歌曲名称一部分后点击确定,则发送消息到服务器端,消息格式为:
| 块大小 | 服务类型’S’ | 用户名 | 搜索歌曲名 |
2.服务器端接受到服务类型’S’后,调用searchsend(QStringusername,QStringsongname)函数,"select*fromsongwheresongnamelike'%"+songname+"%'",先在song表中查询类似于用户输入的字符串的歌曲,然后在该用户表中查询这些歌曲是否已经选为了喜欢:
for(i=0;i<sum;i++)
{
s="select*from"+username+"whereID="+tr("%1").arg(id[i]);
query.exec(s);
if(query.next())
{
liked[i]=true;
}
else
{
liked[i]=false;
}
out<<liked[i];
}
这个主要是为了当用户选择某个歌曲时,可以知道该歌曲是否已被选,给出喜欢不喜欢的显示状态。
3.服务器端返回搜索的信息,格式为:
| 块大小 | 服务类型’S’ | 结果个数 | 第一首歌曲信息 | ……. | 最后一首歌曲信息 | 各歌曲是否已选为喜欢 |
4.客户端收到信息后,用一个歌曲结构体数组存放这些信息,用一个bool数组存放对应歌曲是否已经被选中为喜欢,然后新建一个QwidgetTable的表,将这些结果在表中显示出来,并定义一个函数,用来处理当点击了表格后的赋值、播放等操作。
3.3.6模拟电台
在服务器端的song表里面,对每一首歌曲定义了它所属的类型,在本程序中类型有六个:1:经典老歌,2:DJ舞曲,3:轻音乐,4:非主流,5:民歌,6:网络歌曲,而模拟电台就是根据这些分类进行的。当用户点击电台时,会跳出一个QwidgetTable的选择框,里面有六个选择,即六个歌曲类型,当用户点击了其中的一个类型,电台按钮的显示信息改为“取消电台”,前面说的上一首和下一首的功能就可以根据按钮显示的不同得出当前的电台状态并执行相应的操作,然后发送信息给服务器端,服务器端返回应该播放的歌曲信息。如果用户没有点击类型而是取消了,则一切照旧。如果程序正处于模拟电台状态,当点击取消电台按钮时,仅仅将按钮的显示改为“电台”,当前播放状态不变。
当接着点击取消电台时,该按钮的文本变为电台,这对于下一首和上一首的决断是必要的,因为在本程序中,规定当处于模拟电台阶段,则上一首无效,下一首继续播放该类型歌曲。
模拟电台功能的实现过程是:
1.用户点击电台,程序新建一个QWIdgetTable的对话框,里面显示了与数据库对应的六种类型,供用户选择。
2.当用户选中了其中的一种,则发送消息到服务器端,格式为:
| 块大小 | 服务类型’A’ | 用户名 | 歌曲类型 | 上一曲id |
为了每次更换的播放歌曲不同,就在格式的后面加了上一曲id,则服务器返回的就是大于该id的新的歌曲。
3.服务器接收到类型’A’,调用radiosend(QStringusername,inttype,intid)函数处理。使用"select*fromsongwhereTYPE="+tr("%1").arg(type)+"andid>"+tr("%1").arg(id)+"orderbyidasc"语句得到全部的大于上一曲的id的该类型歌曲,若没有找到,说明上一曲的id已经是最大的,则挑选最小id的歌曲发送。然后挑选出搜索中的最小id号的歌曲,对它进行查询,看它是否已经被选为喜欢,"select*from"+username+"whereID="+tr("%1").arg(id),最后发送信息到客户端,格式为:
| 块大小 | 服务类型’A’ | 歌曲id | 歌曲url地址 | 歌曲名 | 歌手 | 歌词URL地址 | 是否已经喜欢 |

4.客户端收到信息后,对current进行赋值,播放发过来的歌曲,显示歌词,根据是否喜欢决定喜欢/不喜欢按钮显示状态等。在模拟电台阶段,也可以使用其他操作,使用基本的播放、暂停等对它没有影响,如果使用打开本地文件、搜索、推荐功能,则会取消掉电台的状态。
3.3.7推荐歌曲
前面的喜欢、不喜欢功能就是为了给用户进行歌曲推荐而添加的,此播放器的歌曲推荐功能也是在服务器端进行的,它主要是比较当前提出要求的客户与其他客户之间的相似度,取最大相似度的用户中的没有被当前用户选中为喜欢的歌曲。
原理如上图所示。
当用户点击推荐时,客户端发送消息到服务器端,服务器端得到推荐的歌曲列表并返回给客户端,客户端收到后新建一个窗口,将推荐的结果显示出来,并定义一个点击事件,用来播放用户选中的歌曲,因为这些歌曲当前用户并没有选为喜欢,所以显示的是喜欢按钮。
以下是当前数据库中的相关信息:

图3.21 数据一

图3.22 数据二

图3.23 数据三
根据上面三图中的数据使用推荐原理,可以知道会推荐“爱”和“疯狂青蛙”。
下图是点击推荐按钮后的效果图:

图3.24 点击推荐后的效果
从以上两幅图可以看出,当选中推荐歌曲后电台的状态改变了,这是符合要求的。而且推荐结果和理论结果是一致的。

