C语言实现音乐播放器 visual studio 2019

Source

这是一款基于easyx图形库和C/C++编写的音乐播放器,参考了网上许多大佬友情分享的项目设计,结合自己的理解加以设计和制作,给有同样需求的朋友一点参考。文中放上了个人感觉很有帮助的文章,建议可以参考阅读。

项目基础

基于windows和easyx的一款音乐播放器

mciSendString函数是这款音乐播放器能够实现功能的底层函数,其余所有的代码不过是在锦上添花和增加扩展功能,这个是一定要掌握的。(如果能力足够,使用QT这类框架功能更加强大)

easyx是项目所需的图形库,在界面设计上有多很强大的功能和方便的函数。如果想做图形界面可以参考使用。

学习了解mciSendString函数:

有关链表的增删改查操作、文件读写操作、UI的设计、MFC库easyx库

设计思路

  1. 基于Windows控制台C程序的音乐播放器其实是通过调用Windows上的一些API实现的,那就只需要通过函数对mciSendString传输字符串参数为命令控制Windows程序播放MP3文件。

  2. 基于这个想法,可以对音乐播放器的各个功能模块在原生API的基础上再加工,封装成一个个小demo,然后再做一个简陋的界面去操控。

  3. 那么一切合理了起来:也就是说相当于把各种本来要手打的命令行变成按钮或者键盘输入字符,按一下就往这里面输出一些命令,也就能实现音乐播放器了。但是

    • 文件不可能飞过来让你使用,那么该怎么读取呢?
    • 有没有办法可以指定文件读取?批量导入?
    • 既然要读取,那么用什么东西来装它呢?
    • 装进去了能不能删除、增添、改变?
    • 能不能读取装进去之后里面的信息?
  4. (显然)由题可知,我们可以使用链表作为这种数据结构(数据量不大),方便完成各种操作。那链表存储可以实现了,文件又怎么读取呢?

  5. 那当然是采用直接呼出文件资源管理器的方式去读取文件的路径,这个的实现我们之后再说,这样也更加友好和人性化,有图形界面的操作手感

    • 文件读取的时候总是读取失败是为什么
    • 通过断点发现文件路径参数是正确的,为什么mci无法成功播放呢?
    • 呼出管理器后操作返回数据为空有没有处理?
  6. 所以,需求里难弄的的东西来了。图形界面,用C做图形界面,完全陌生的领域。摆在面前的有几种选择:

    • Easyx.h图形库
    • MFC框架
    • QT框架
    • WPF框架

    这些都是在C环境下做UI的好点子。但是MFC太拉了,UI奇丑,代码可读性差的离谱。人家微软自己都放弃了我还用吗(其实是太难不会)。QT可以说是功能强大齐全,但是学习成本高,或者说一个小播放器还不至于用QT,有点大材小用。WPF是基于C#的。

    那么综上所述,我们组采取Easyx.h图形库方案,UI这种东西全靠审美。审美上去了拿什么做都好看,也就是说只要你会PS,界面这种东西不成问题,靠队友的时候到了。

  7. 那么这时根据需求,我们所有的技术选择都已经齐全,可以开始着手写代码了。

  • HwcPlayer中有一个很不错的点子,把要用的所有include全写在一个头文件里每个都引一次,很不错的想法,懒人必备噢。

一些基础的知识不再赘述,以下是一些踩过的坑(本项目),具体代码在文末链接中可以自行下载

UI背后的逻辑问题

  • 函数接收什么样的参数?
  • 命令都是什么样的?
  • 如何组合字符串成为一个命令?
  • 如何读取文件并输入到播放列表中?
  • 每一个功能需要几个API?
  • 什么是alias别名?如何通过别名简化命令?
  • 这些API尽可能多地能组成什么样的功能?
  • ~~所以。。有谁会做UI吗?~~还是决定自己做吧

解决方案

mci函数参数
MCIERROR mciSendString(
      LPCTSTR lpszCommand,    //MCI命令字符串
      LPTSTR lpszReturnString, //存放反馈信息的缓冲区
      UINT cchReturn,          //缓冲区的长度
      HANDLE hwndCallback  //回调窗口的句柄,一般为NULL
); //若成功则返回0,否则返回错误码。
MCI命令
命令 解释
open 打开设备
close 关闭设备
play 开始设备播放
stop 停止设备的播放或记录
record 开始记录
save 保存设备内容
pause 暂停设备的播放或记录
resume 恢复暂停播放或记录的设备
seek 改变媒体的当前位置
status 查询设备状态信息
capacility 查询设备能力
info 查询设备的信息
将字符串相互拼接产生可以被系统理解的MCI指令
  • strcpy和strcat任君选择
strcpy: char *strcpy(char* dest, const char *src);
//把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间
//src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串
//返回指向dest的 指针。
strcat: extern char *strcat(char *dest, const char *src);
//把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')
呼出文件资源管理器实现读取文件路径并存放到指定链表节点
  • 需要注意最下方链表插入的路径不能为空,加一个if判断防止插入空数据报错
void add()
{
    
      
    TCHAR szBuffer[MAX_PATH] = {
    
       0 };
	OPENFILENAME file = {
    
       0 };
	file.hwndOwner = NULL;
	file.lStructSize = sizeof(file);
	file.lpstrFilter = "*文件(*.*)\0*.*\0";//要选择的文件后缀
	file.lpstrFile = szBuffer;//存放文件的缓冲区
	file.nMaxFile = sizeof(szBuffer) / sizeof(*szBuffer);
	file.nFilterIndex = 0;
	file.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER;//标志如果是多选要加上OFN_ALLOWMULTISELECT
	BOOL bSel = GetOpenFileName(&file);
	Media* music = createMedia(file.lpstrFile);//创建的链表
	if (music != NULL) {
    
      
		headInsert(list, *music);
	}	
}
获取播放信息和播放位置
命令 解释
“status movie position” 播放位置
“status movie length” 播放总长度
“status movie mode” 播放状态
"seek movie to " 指定位置
“seek movie to start” 定位到开头位置
“seek movie to end” 定位到最后位置
alias别名
  • 为长命令设置别名更有助于写代码

  • 由于对MCI的操作实际上就是命令操作,所以可以通过设置别名来进行操作mcisendstring函数

@doskey ls=dir /b $*
@doskey l=dir /od/p/q/tw $*
//命令 alias 别名
@REM notepad++工具设置别名为:npp
@doskey npp="C:\Program Files1\Notepad++\notepad++.exe" $   

一些难点(许是太菜了)

  • 控制台页面时期的双缓冲刷新已经是过去式了
  • 文件读取路径包含空格无法读取
  • 通过呼出文件资源对话框实现导入文件(上文已提到)
  • 对话框式导入文件和easyx界面刷新冲突
  • 如何通过图片实现伪造的按钮特效
  • easyx图形界面的进度条和刷新页面的矛盾

解决方案

双缓冲原理有些复杂,命令行使用一个更简单的方案
  • 这种刷新是通过“不刷新”实现的,当字符在相同位置更新的时候会出现上一次未刷新而字符重叠的现象。可以通过使用长空白字符覆盖未刷新区域来实现“伪刷新”
//消除cls频闪函数
void cls() {
    
      
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD coordScreen = {
    
       0, 0 };    // home for the cursor
	SetConsoleCursorPosition(hConsole, coordScreen);
}
MCI文件路径空格问题
  • mci读取文件的路径如果包含空格需要采用双引号包住整个路径
int openMusic(const char* url)
{
    
      
	if (url != NULL) 
	{
    
      
		char cmd[500] = "";
		strcpy_s(cmd, "open \"");//引号需要被转义
		strcat_s(cmd, url);
		strcat_s(cmd, "\"");//引号需要被转义
		strcat_s(cmd, " alias MyMusic");
		if (mciSendStringUtil(cmd, NULL) == 0)
		{
    
      
			return 0;
		}
	}
}
对话框式导入文件和easyx界面刷新冲突
  • 一个奇怪的bug,最后通过绝对路径解决,有大佬知道问题的可以评论区见
  1. 执行如下呼出文件资源管理器代码后正常
void add() {
    
      
	TCHAR szBuffer[MAX_PATH] = {
    
       0 };
	OPENFILENAME file = {
    
       0 };
	file.hwndOwner = NULL;
	file.lStructSize = sizeof(file);
	file.lpstrFilter = "*文件(*.*)\0*.*\0";//要选择的文件后缀
	file.lpstrFile = szBuffer;//存放文件的缓冲区
	file.nMaxFile = sizeof(szBuffer) / sizeof(*szBuffer);
	file.nFilterIndex = 0;
	file.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER;//标志如果是多选要加上OFN_ALLOWMULTISELECT
	BOOL bSel = GetOpenFileName(&file);
	Media* music = createMedia(file.lpstrFile);
	if (music != NULL) {
    
      
		headInsert(list, *music);
	}
}
  1. 再执行刷新界面
HWND hwnd = initgraph(720, 960, 0);
//或者屏幕刷新等类似函数
loadimage(&play2, ".\\*.png", 81, 81);
loadimage(&pause, ".\\*.png", 81, 81);
  • 结果:页面变黑,有鼠标点击事件,图片无法加载
  1. 解决方案:所有加载的图片都指定绝对路径
loadimage(&play2, "D:\\visual studio project\\Project_List\\Project_List\\picture\\*.png", 81, 81);
loadimage(&pause, "D:\\visual studio project\\Project_List\\Project_List\\picture\\*.png", 81, 81);
伪造按钮特效
  • 提前准备好两张透明背景的png图片,执行鼠标点击动作时两种不同颜色的图层依次刷新,就可以实现鼠标点击的伪动态效果
void transimg(IMAGE* dstimg, int x, int y, IMAGE* srcimg)
{
    
      
	HDC dstDC = GetImageHDC(dstimg);
	HDC srcDC = GetImageHDC(srcimg);
	int w = srcimg->getwidth();
	int h = srcimg->getheight();
	BLENDFUNCTION bf = {
    
       AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
	//半透明位图
	AlphaBlend(dstDC, x, y, w, h, srcDC, 0, 0, w, h, bf);
}
transimg(NULL, 334, 652, &IMAGE);//一种颜色的按钮
Sleep(100);
transimg(NULL, 334, 652, &IMAGE);//另一种颜色的按钮
进度条的制作
  • 刷新页面与刷新进度条
//进度条
while(1)
{
    
      
    t = getMusicLength();
		t1 = getMusicPosition();
		if (t != 0)
        {
    
      
            c = t1 / t;
        }
		progress_length = c * 523 + 90;
		fillroundrect(90, 830, progress_length, 825, 10, 10);
}

Github项目地址 — Music-Player