首先,祝贺阿根廷获得2022世界杯冠军!
文章目录
简介
Winform作为一个比较老的平台,应用其实越来越少了,而即使设计Winform程序,多数人也会选择C#,而不是C++。但是题主在学校学习一门课程被迫使用了Winform/C++,并完成了课程作业,在此分享以下自己的作业,也当作学习纪录。在完成这个播放器过程中,我也参考了网上许多资料,最主要的可能是下面这位大佬做的三篇文章,不过他是用C#完成的,且没有加入在线的搜索播放功能(但是做的属实很好):
C#-WinForm-简单的音频播放器(基于WindowsMediaPlayer控件)(一)
C#-WinForm-简单的音频播放器(基于WindowsMediaPlayer控件)(二)
C#简单音乐播放器(三)
我的播放器同样是通过WMP控件实现,具备的主要功能有导入并播放本地歌曲,读取本地歌词文件,搜索、播放、收藏并下载在线歌曲。另外,由于老师要求使用到数据库操作,还加入了用户注册并登录功能,每个用户都能设置单独的播放器主题和歌单。这个播放器最终也实现成功打包为一个安装包,便于在其它电脑安装使用。下图是播放器的整体界面及注释:
功能展示
1.用户注册、登录、自定义主题
2.本地歌曲导入、播放并读取歌词文件
歌词文件需要为.lrc格式,且与歌曲同名并放在同个路径下
3.在线歌曲搜索、收藏、播放
4.歌词同步及桌面歌词
5.在线歌曲下载
一、新建Winform项目
介绍完主要功能就要进入到正式设计了,首先当然要新建一个Winform的空项目,这个步骤网上的教程其实很多,前人之述备矣。我采用的是新建CLR空项目后新建窗体再重启VS的方法(不得不吐槽经常出现窗体加载不出来的问题),这里要注意不同版本的VS可能操作过程不同,我使用的是2017版的,同个版本可以使用下面这位兄弟的方法,不过里面的那段代码要改一下:
#include "MyForm.h"
using namespace Project1;
[STAThread]
void Main(array<String ^>^ args)
{
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);
MyForm form;
Application::Run(%form);
}
提示:工程名和窗体名要根据自己的项目更改,另外我发现工程名一般设为Project加数字就行,改成别的容易运行失败
二、界面UI设计
主要控件:
播放器:AxWMPLib::AxWindowsMediaPlayer
控制按钮:PictureBox
菜单栏按钮:Panel+PictureBox
提示文本:Label
搜索框:TextBox
搜索数设置:NumericUpDown
列表:ListView
进度条和音量条:TrackBar
1.按钮控件
按钮是本项目中最常用的控件,简介图片中蓝色字体的注释均为此控件,但由于Winform自带的按钮控件外观实在有些落后,我选择使用PictureBox控件搭配图片进行设计,添加过程为:新建PictureBox控件->Name属性命名->Image属性添加图片->BackColor设置颜色(一般为透明使其与窗体同色)->SizeMode设置为Zoom->Cusor设置为Hand(鼠标的图标)->添加Click,MouseEnter,MouseMove触发事件(分别表示点击、悬停、离开)。下面以播放按钮为例:
按钮使用时的效果:
下面是该按钮需要添加的触发函数:
//鼠标悬停 播放 按钮
private: System::Void Play_Enter(System::Object^ sender, System::EventArgs^ e) {
if ((int)MusicPlayer->playState != 3) {
PlayBtn->Image = (cli::safe_cast<System::Drawing::Image^>(rm->GetObject(L"播放pink")));
}
else if ((int)MusicPlayer->playState == 3) {
PlayBtn->Image = (cli::safe_cast<System::Drawing::Image^>(rm->GetObject(L"暂停pink")));
}
}
//鼠标离开 播放 按钮
private: System::Void Play_Leave(System::Object^ sender, System::EventArgs^ e) {
if ((int)MusicPlayer->playState != 3) {
PlayBtn->Image = /*gcnew Bitmap(Application::StartupPath + "\\Icon\\播放white.png");*/(cli::safe_cast<System::Drawing::Image^>(rm->GetObject(L"播放white")));
}
else if ((int)MusicPlayer->playState == 3) {
PlayBtn->Image = (cli::safe_cast<System::Drawing::Image^>(rm->GetObject(L"暂停white")));
}
}
//鼠标点击 播放 按钮
private: System::Void Play_Click(System::Object^ sender, System::EventArgs^ e) {
if ((int)MusicPlayer->playState == 2)//如果播放器暂停
{
MusicPlayer->Ctlcontrols->play();//开始播放
}
else if ((int)MusicPlayer->playState == 3)//如果播放器正在播放
{
MusicPlayer->Ctlcontrols->pause();//暂停播放
}
}
具体的实现过程可以参考我的一篇教程:
Winform/C++按钮控件自定义外观、触发函数
本项目中还有菜单栏按钮(简介图红色注释对应按钮),此类按钮实现原理相似,不同的是我将用PictureBox制作的按钮放在一个Panel控件中,使用时通过改变Panel的BackColor来实现按钮反馈,方法较为简单,此处不再赘述。
2.Windows Meida Player控件
由于设计的是一个音乐播放器,因此自然少不了具有播放功能的控件。关于这个控件的使用网上也已经有了不少资料,要添加这个控件有两种方法,一种是直接拖动控件添加,值得注意的是,WMP控件并不在默认的工具箱中,因此我们需要在工具箱中右击鼠标,在弹出菜单中单击“选择项”,在弹出的“选择工具箱项”对话框中单击“COM组件”选项卡,选择“Windows Media Player”,点击确定添加,再在工具箱中把该控件拖放到窗体中。另一种方法是在程序中通过编写代码添加,好处是更为灵活,也不会在设计器中出现控件。
在MainForm.h中添加命名空间 using namespace System::Media;
在MainForm类中生成播放器的对象: private: AxWMPLib::AxWindowsMediaPlayer^ MusicPlayer = gcnew AxWMPLib::AxWindowsMediaPlayer;
在MainForm类中FormLoad函数中(避免设计器出现控件)
MusicPlayer->BeginInit();
this->MusicPlayer->Visible = false;
MusicPlayer->EndInit();
在触发函数中调用此播放器如:
MusicPlayer->URL = Path;
MusicPlayer->Ctlcontrols->play();
添加控件后,由于我们不打算使用默认的UI,因此需要将控件的Visible属性设置为False。
关于WMP控件的属性及函数等,网上有很多资料,这里推荐一篇文章以作参考:
c#—如何借助windows media player控件编写播放器
3.桌面歌词
桌面歌词我使用的方法是新建一个窗体,窗体上只有一个Label控件用来显示歌词。窗体属性的设置流程如下:
- 将窗体的FormBorderSyle设置为None(去掉边框)
- 将窗体的BackColor设置为某个特定颜色(除了Transparent)
- 将窗体的TransparencyKey设置为上面设置的颜色(意味着窗体中这种颜色的区域会呈现透明效果
- 将Label的BackColor也设置为此颜色
- Label的ForeColor设置为另一种不同的颜色
设置完成后即可以在主窗体MainForm.h中调用该窗体类生成对象如:
public:LrcForm^ lrcform = gcnew LrcForm();//桌面歌词窗体
4.界面全屏显示
在运行程序时,控制窗体大小的按钮(最小化、最大化、关闭)控件功能实现基本只需要通过改变窗体的WindowState即可,如最大化窗体:
this->WindowState = FormWindowState::Maximized;
但是,实际操作时我们会发现,最大化后窗体内的控件位置会集中在左上角,无法实现真正的全屏效果。解决这个问题,我们主要通过设置控件的anchor属性和Dock属性,这两个属性的基本含义是:
anchor:表示控件与窗体的位置关系,有left、top、bottom、right、none五个参数。激活任意一个即表示控件与窗体某一边绝对位置保持不变,可同时激活多个。
dock:表示控件填充窗体某一边。
例如,播放按钮我们希望在任何时候都位于界面的左下角,那么只需要把anchor设置为Bottom+Left。以此类推,设置其它控件后即可呈现全屏效果,如下所示:
二、主要功能实现
这个音乐播放器实现的功能比较杂,这篇文章很难都详细涉及,只能随缘选一些纪录一下了。
1.数据库操作(Access)
数据库在本项目中还是比较重要的,在此特别展开讲一下。数据库方面使用的是Access数据库,一共使用了两个数据库:用户信息(UserData)数据库和歌曲信息(LocalSongs)数据库,分别用来存放用户信息和歌曲信息。如下图所示:
数据库的链接和使用同样有两种方法。
一种是在窗体可视化设计界面添加特定的控件(如下图)并设置链接数据库,再在程序文件中调用这些控件从而实现对数据库的增删查改等操作。这种方法在网上能查到很多资料,此处不再赘述。
另一种是在程序文件中直接使用代码的方式调用数据库的链接并使用,代码主要框架如下(注意数据库放到程序debug文件夹中,便于最后打包):
private:String^ strConn2= String::Format("Provider=Microsoft.Jet.OLEDB.4.0; Data Source={0}\\UserData.mdb", Application::StartupPath);
private:OleDbConnection^ Conn2 = gcnew OleDbConnection(strConn2);
Conn2->Open();
String^ cmd=······;//sql语句
Data::OleDb::OleDbCommand^ selectCmd = gcnew Data::OleDb::OleDbCommand(strcmd, Conn2);
······//根据需要编写,如Object^ oRes = selectCmd->ExecuteScalar();返回的oRes可表示真/假
Conn2->Close();
这两种方法各有优劣,前者链接过程较为简单,可视化操作,缺点是所连接的数据库在程序运行时较难更改;后者需要在程序中通过代码新建数据库连接,但优点是便于修改所连接的数据库,另一方面,后者更易实现最终程序的打包操作。本项目主要采用后者,部分使用了前者。
个人也较为推荐第二种方法。
进行数据库操作时我们常常会用到SQL语句,这些在网上也都能找到资料,这里也推荐一个网址查询:
SQL语法
下面以本项目中注册按钮的数据库操作为例子(如下代码是注册按钮的Click函数),由于
在点击注册按钮时需要实现写入用户数据并创建歌曲信息表,即需要操作两个数据库文件,因此我选择编写了一个SqlWrite(String^ mdbName,String^ strcmd)函数,在函数中定义了可以随mdbName(数据库名)变化的链接,这里就体现了通过代码添加数据库链接的优越性。
//注册按钮
private: System::Void SignUp_Click(System::Object^ sender, System::EventArgs^ e) {
if (!IfCheckFill()) {
NameGuideLabel->Text = "用户名和密码不能为空!";
NameGuideLabel->Visible = true;
return;
}
userName = UserNameTextbox->Text->Trim();
passWord = PassWordTextbox->Text->Trim();
String^ strCmd = String::Format("SELECT UserName FROM {0} WHERE UserName='{1}'", "UserData", userName);
this->Conn1->ConnectionString = String::Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\\{1}.mdb", Application::StartupPath, "UserData");
Data::OleDb::OleDbCommand^ selectCmd = gcnew Data::OleDb::OleDbCommand(strCmd, Conn1);
// 执行操作
this->Conn1->Open();
Object^ oRes = selectCmd->ExecuteScalar();
this->Conn1->Close();
if (oRes) {
this->NameGuideLabel->Text = "×用户已存在!";
NameGuideLabel->Visible = true;
}
else
{
//将用户名和密码存入用户数据表格中
String^ strCmd1 = String::Format("INSERT INTO UserData(UserId,UserName) VALUES('{0}','{1}')", passWord, userName);
SqlWrite("UserData",strCmd1);
//创建用户专属表格(下载歌单和收藏歌单)
String^ strCmd2 = String::Format("CREATE TABLE {0}DownloadedList(FilePath varchar(255),SongName varchar(255),Singer varchar(255),Duration varchar(255),FileSize varchar(255))",userName);
SqlWrite("LocalSongs", strCmd2);
String^ strCmd3 = String::Format("CREATE TABLE {0}FavoriteList(FilePath varchar(255),SongName varchar(255),Singer varchar(255),Duration varchar(255),FileSize varchar(255))", userName);
SqlWrite("LocalSongs", strCmd3);
NameGuideLabel->Text = "注册成功!";
NameGuideLabel->ForeColor = Color::DarkGreen;
NameGuideLabel->Visible = true;
}
}
//检查文本框是否为空的函数
private:bool IfCheckFill() {
userName = UserNameTextbox->Text->Trim();
passWord = PassWordTextbox->Text->Trim();
if (String::IsNullOrEmpty(userName))return false;
if (String::IsNullOrEmpty(passWord))return true;
else return true;
}
//数据库操作函数,输入参数为数据库名和指令
private:void SqlWrite(String^ mdbName,String^ strcmd)
{
this->Conn1->ConnectionString =String::Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\\{1}.mdb",Application::StartupPath,mdbName);
Data::OleDb::OleDbCommand^ selectCmd = gcnew Data::OleDb::OleDbCommand(strcmd, Conn1);
// 执行操作
this->Conn1->Open();
Object^ oRes = selectCmd->ExecuteScalar();
this->Conn1->Close();
}
2.在线功能
仅供学习交流,不做商业用途
在线歌曲方面,Winform提供了联网功能,不过需要先添加引用System.Web.Extensions
如果在引用里找不到的话,需要先将项目属性(右键Project->Porperties)按下图设置为V4.0再添加引用,添加完引用可以再改回来。
当然,这样还不够,在程序中,我们还需要"Using"一些引用,如下所示:
using namespace System;
using namespace System::ComponentModel;
using namespace System::Collections;
using namespace System::Windows::Forms;
using namespace System::Data;
using namespace System::Drawing;
using namespace System::Collections::Generic;
using namespace System::Net;
using namespace System::IO;
using namespace System::Text;
using namespace System::Web::Script::Serialization;
using namespace System::Collections::Generic;
音源上,我使用的是酷我音乐的音源,获取的方法在网上基本也都能找到,以下列出主要的几个操作:
-
歌曲搜索:(返回含Rid的Json字符串,需要进行反序列化处理)
Get(http://search.kuwo.cn/r.s?all=str&ft=music&rformat=json&encoding=utf8&rn=30&callback=song&vipver=MUSIC_8.0.3.1) -
歌曲.mp3链接获取:(将获取的链接直接赋给MusicPlayer->URL,再播放
Get(http://antiserver.kuwo.cn/anti.s?response=url&type=convert_url&br=320kmp3&format=mp3&rid=MUSIC_MusicRid) -
歌词获取:
Get(http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=MusicRid)
其中加了粗体的都是需要根据需求自己设置的,str表示搜索框的字符串,可以搜索歌手或歌曲;rn的值表示搜索数目;Musicrid的值表示歌曲对应的RID,即第一步中获取。
关于Get(String^ str)方法(http请求方法)和json反序列化的实现,网上也有不少资料,不过基本也都是C#编写的,稍作修改就好了。
下面给出我修改了的Get方法:
//获取歌曲json类型数据,返回json字符串
public:static String^ Get(String^ Url) {
HttpWebRequest^ request = (HttpWebRequest^)WebRequest::Create(Url);
request->Proxy = nullptr;
request->KeepAlive = false;
request->Method = "GET";
request->ContentType = "application/json; charset=UTF-8";
request->AutomaticDecompression = DecompressionMethods::GZip;
HttpWebResponse^ response = (HttpWebResponse^)request->GetResponse();
Stream^ myResponseStream = response->GetResponseStream();
StreamReader^ myStreamReader = gcnew StreamReader(myResponseStream, Encoding::UTF8);
String^ retString = myStreamReader->ReadToEnd();
myStreamReader->Close();
myResponseStream->Close();
if (response != nullptr)
{
response->Close();
}
if (request != nullptr)
{
request->Abort();
}
return retString;
}
下面是我定义的在线搜索方法,只需要在点击搜索按钮调用即可:
//在线搜索函数(json)
private:void OnlineSearch() {
if (String::IsNullOrEmpty(SearchTextBox->Text->Trim())|| SearchTextBox->Text->Trim()=="在线搜索") {
ExitForm^ form = gcnew ExitForm();
form->GuideLabel->Text = "请输入歌曲或歌手!";
form->StartPosition = FormStartPosition::CenterParent;
form->ShowDialog();
return;
}
OnlineSongslist->Clear();
String^ searchNumber = SearchNumberChoose->Text;
String^ searchName = Get(String::Format("https://search.kuwo.cn/r.s?all={0}&ft=music&itemset=web_2013&client=kt&pn=0&rn={1}&rformat=json&encoding=utf8&vipver=MUSIC_8.0.3.1", SearchTextBox->Text->Trim(),searchNumber));
array <String^>^ ss = gcnew array <String^>{searchName};//将获取的json字符串放入字符串数组中,每首歌对应一个字符串
//产生搜索提示窗口,并在搜索完成后关闭窗口
ExitForm^ form = gcnew ExitForm();
form->OKBtn->Visible = false;
form->CancelBtn->Visible = false;
form->CloseBtn->Visible = false;
form->GuideLabel->Visible = true;
form->GuideLabel->Text = "(*^_^*)正在搜索歌曲······";
form->ShowInTaskbar = false;
form->StartPosition = FormStartPosition::CenterScreen;
form->Show();
form->Update();//这条指令能确保搜索时提示框能显示
//反序列化处理字符串
for each(String^ s in ss)
{
JavaScriptSerializer^ serializer = gcnew JavaScriptSerializer();
Dictionary<String^, Object^>^ json = (Dictionary<String^, Object^>^)serializer->DeserializeObject(s);
array<Object^>^ firstKey = (array<Object^>^)json["abslist"];
for each(Object^ i in firstKey)
{
Dictionary<String^, Object^>^ y = (Dictionary<String^, Object^>^)i;
String^ path = "http://antiserver.kuwo.cn/anti.s?response=url&type=convert_url&br=320kmp3&format=mp3&rid=" + y["MUSICRID"]->ToString();//hts_MVPIC
String^ mp3Path = Get(path);//mp3后缀路径
String^ songname = y["SONGNAME"]->ToString();//歌曲名
String^ singername = y["ARTIST"]->ToString();//歌手名
int During = int::Parse(y["DURATION"]->ToString());//歌曲时长-秒
int Min = During / 60;
int Sec = During % 60;
String^ min;
String^ sec;
if (Min >= 10)min = Min.ToString();
else if (Min < 10)min = "0" + Min;
if (Sec >= 10)sec = Sec.ToString();
else if (Sec < 10)sec = "0" + Sec;
String^ during = min + ":" + sec;//歌曲时长-00:00
OnlineSongslist->Add(gcnew SongsList(songname, singername, path, during,"未知"));
}
}
form->Close();
localSongslist = OnlineSongslist;
AddSongsToListView(localSongslist);
}
上面这段代码中除了获取了歌曲链接和歌曲信息外,还通过Songslist生成歌曲对象存放这些信息,Songslist是自己定义的一个类,只需要在类中定义所需要的属性和构造函数就可以,这里我感受到了面向对象编程的优越性。类似的方法我也用在了歌词处理上。下面是我的歌曲类的一些属性:
//定义索引属性,方便外部调用
public: property String^ FileName { String^ get() { return fileName; } void set(String^ value) { fileName = value; } }
public: property String^ FilePath { String^ get() { return filePath; } void set(String^ value) { filePath = value; } }
public: property String^ FileSize { String^ get() { return fileSize; } void set(String^ value) { fileSize = value; } }
public: property String^ SingerName { String^ get() { return singerName; } void set(String^ value) { singerName = value; } }
public: property String^ SongDuration { String^ get() { return songDuration; } void set(String^ value) { songDuration = value; } }
public: property int SongId { int get() { return songId; } void set(int value) { songId = value; } }
public: property bool IfFavorite { bool get() { return ifFavorite; } void set(bool value) { ifFavorite = value; } }//是否被添加到最爱歌单
public: property String^ RidPath { String^ get() { return ridPath; } void set(String^ value) { ridPath = value; } }
3.程序打包
程序的打包我参考的是下面这篇教程进行的,使用的是Visual Studio Installer。
[WinForm] VS2010发布、打包安装程序(超全超详细)
其中,这篇教程的一些操作并不是必要的,可以根据自己的需求选择。另外,我第一次尝试打包程序后在安装运行时报出了缺少动态链接库文件(dll)的错误,这种问题只需要在打包时将对应的dll文件添加进去即可(部分在自己电脑系统文件中可以找到)。除此之外,为了方便更新,我们可以给安装包设置版本号,不过每次更改版本号会导致ProductCode的改变。
另外,由于每次更新时用户的歌单数据库和用户信息数据库是不需要更改的,我们可以将这两个文件的属性Permanent设置为True,如下图所示:
程序完成打包后即可进行安装操作。
总结
这个音乐播放器在运行时仍然会存在一些小bug,不过主要是出现在在线歌曲的处理上,我自己专业也不是学软件的,因此对很多东西也一知半解,包括面向对象编程,数据库处理等也是刚刚基础,只是因为比较有趣才花了不少时间学习。在此留个记录,如果对其他读者也有帮助的话,那更是我的荣幸。