音乐播放器之源码解析三
来源:互联网 发布:衣柜设计软件用哪款好 编辑:程序博客网 时间:2024/05/18 02:04
mpxUI代码解析
全局变量
/****************************************************************************** author menghun3@gmail.com** This program is free software; you can redistribute it and/or modify* it under the terms of the GNU General Public License as published by* the Free Software Foundation.* * source code address https://github.com/menghun3/mpx******************************************************************************/#include <windows.h>#include <CommCtrl.h>#include <stdio.h>#include <strsafe.h>#include <conio.h>#include <ShlObj.h>#include "mpx.h"#include "playlist.h"#include "lyric.h"#pragma comment (lib, "Comctl32.lib")#pragma comment (lib, "mpx.lib")#pragma comment (lib, "winmm.lib")// 版本号#define MPXUI_MAJOR_VERSION 0#define MPXUI_MINOR_VERSION 1#define MPXUI_REVISION_VERSION 0#define MPXUI_BUILD_VERSION 0//component http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx//control library http://msdn.microsoft.com/en-us/library/windows/desktop/bb773169(v=vs.85).aspxtypedef enum{C_BTN_PLAY = 1,C_BTN_PAUSE,C_BTN_PREV,C_BTN_NEXT,C_BTN_STOP,C_BTN_FROM_PL,C_LYRIC_STATIC,C_TIME_STATIC,C_PL_ADD,C_PL_ADDDIR,C_PL_DEL,C_PL_SAVE,C_LAST}COMPONENT_ID;HINSTANCE g_hInst = NULL;HWND g_hWnd = NULL;int playIndex = 0;static int status = -1; // C_BTN_PLAY:play;C_BTN_PAUSE:pause;C_BTN_STOP:stopUINT g_timeId = 0;
全局变量中包含表示播放状态的宏和表示组件的宏
主界面函数
int APIENTRY WinMain(HINSTANCE hInstance, //应用程序的实例句柄,HINSTANCE hPrevInstance,LPSTR lpCmdLine, //命令行int nCmdShow) //显示方式{MSG msg;HWND hWnd;char szTitle[]="mpx"; // The title bar textWNDCLASSEX wcex={0};g_hInst = hInstance;wcex.cbSize = sizeof(WNDCLASSEX); //WNDCLASSEX结构体大小wcex.style = CS_HREDRAW | CS_VREDRAW; //位置改变时重绘wcex.lpfnWndProc = (WNDPROC)WndProc; //消息处理函数wcex.hInstance = 0; //当前实例句柄wcex.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME; //背景色wcex.lpszClassName = "mpxclass"; //参窗口类名wcex.hIcon =0; //图标wcex.hCursor =LoadCursor(NULL, IDC_ARROW); //光标wcex.lpszMenuName =0; //菜单名称wcex.hIconSm =0; //最小化图标RegisterClassEx(&wcex); //注册窗口类hWnd = CreateWindowEx(WS_EX_ACCEPTFILES,"mpxclass", szTitle, WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN|WS_VISIBLE, //创建窗口CW_USEDEFAULT,CW_USEDEFAULT,800, 400, NULL, NULL, 0, NULL);if (!hWnd){return FALSE;}g_hWnd = hWnd;while (GetMessage(&msg, NULL, 0, 0)) // 消息循环:{TranslateMessage(&msg); //转化虚拟按键到字符消息DispatchMessage(&msg); //分派消息调用回调函数}return msg.wParam;}
创建一个800*400的窗口,其他设置参考MSDN例子。
各个组件全局变量
HWND btnPrev = NULL;HWND btnPlay = NULL;HWND btnStop = NULL;HWND btnNext = NULL;HWND playlistLB = NULL;HWND lyricStatic = NULL;HWND timeStatic = NULL;HWND btnPLAdd = NULL;HWND btnPLAddDir = NULL;HWND btnPLDel = NULL;HWND btnPLSave = NULL;
包含前一曲播放按钮、播放/暂停按钮、停止按钮、下一曲播放按钮、播放列表组件、歌词显示组件、显示播放进度时间组件、添加歌曲按钮、添加文件夹歌曲按钮、删除歌曲按钮、保存歌曲按钮。这些组件在创建窗口时创建。
创建组件部分代码如下,分别调用对应的组件创建函数。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){PAINTSTRUCT ps;HDC hdc;RECT rect;static RECT oldRect;switch (message) {……case WM_CREATE:{GetWindowRect(hWnd, &oldRect);CreateCtrlBtn(hWnd);CreateListBox(hWnd);CreateLyricStatic(hWnd);CreateTimeStatic(hWnd);CreatePlayListBtn(hWnd);}break;……default:return DefWindowProc(hWnd, message, wParam, lParam);}return 0;}
CreateCtrlBtn
创建控制歌曲播放按钮函数。
void CreateCtrlBtn(HWND hWnd){btnPrev = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "prev", WS_VISIBLE|WS_CHILD, 0, 0, 50, 30, hWnd, C_BTN_PREV, g_hInst, NULL);btnPlay = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "play", WS_VISIBLE|WS_CHILD, 60, 0, 50, 30, hWnd, C_BTN_PLAY, g_hInst, NULL);btnStop = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "stop", WS_VISIBLE|WS_CHILD, 120, 0, 50, 30, hWnd, C_BTN_STOP, g_hInst, NULL);btnNext = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "next", WS_VISIBLE|WS_CHILD, 180, 0, 50, 30, hWnd, C_BTN_NEXT, g_hInst, NULL);}
其中C_BTN_PREV表示该组件ID,该使用方式不正式。
CreateListBox函数
创建播放列表
void CreateListBox(HWND hWnd){int i = 0;POINT p = {10, 70};int ident = 0;int plItemCount = 0;int colWidth = 0;int colMaxWidth = 300;RECT rect;GetWindowRect(hWnd, &rect);PlayListInit();plItemCount = GetDefaultPlaylistTotalItem();playlistLB = CreateWindowEx(WS_EX_ACCEPTFILES, WC_LISTBOX, "default", WS_VISIBLE | LBS_COMBOBOX | WS_CHILD|WS_VSCROLL|LBN_DBLCLK|LBS_NOTIFY, 0, 70, 380, rect.bottom - rect.top - 120, hWnd, C_BTN_FROM_PL, g_hInst, NULL); for (i = 0; i < plItemCount; i++){char *val = NULL;char *pval = NULL;char plItem[MAX_PATH] = {0};val = GetItemFromDefaultPlaylist(i);pval = strrchr(val, '\\');if (pval == NULL){pval = val;}else{pval++;}sprintf(plItem, "%03d.%s", i+1, pval);SendMessage(playlistLB, LB_INSERTSTRING, (-1), plItem);}}
调用PlayListInit()初始化默认播放列表,然后通过GetItemFromDefaultPlaylist函数获取所有歌曲路径并将其插入到播放列表组件中。
PlayListInit()实现见播放列表文件。
CreateLyricStatic函数
创建歌词组件
void CreateLyricStatic(HWND hWnd){RECT rect;GetWindowRect(hWnd, &rect);lyricStatic = CreateWindowEx(WS_EX_ACCEPTFILES, WC_STATIC, "lyric", WS_VISIBLE | WS_CHILD|WS_VSCROLL|LBS_NOTIFY|SS_CENTER, 400, 70, 380, rect.bottom - rect.top - 120, hWnd, C_LYRIC_STATIC, g_hInst, NULL);}
CreateTimeStatic函数
创建显示播放时间的组件
void CreateTimeStatic(HWND hWnd){timeStatic = CreateWindowEx(WS_EX_ACCEPTFILES, WC_STATIC, "00:00->00:00", WS_VISIBLE | WS_CHILD|LBS_NOTIFY|SS_CENTER, 240, 0, 100, 30, hWnd, C_TIME_STATIC, g_hInst, NULL);}
CreatePlayListBtn函数
创建添加歌曲等按钮
void CreatePlayListBtn(HWND hWnd){RECT rect;int yPos = GetWindowRect(hWnd, &rect);yPos = 40;btnPLAdd = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "add", WS_VISIBLE|WS_CHILD, 0, yPos, 50, 20, hWnd, C_PL_ADD, g_hInst, NULL);btnPLAddDir = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "dir", WS_VISIBLE|WS_CHILD, 60, yPos, 50, 20, hWnd, C_PL_ADDDIR, g_hInst, NULL);btnPLDel = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "del", WS_VISIBLE|WS_CHILD, 120, yPos, 50, 20, hWnd, C_PL_DEL, g_hInst, NULL);btnPLSave = CreateWindowEx(WS_EX_ACCEPTFILES, WC_BUTTON, "save", WS_VISIBLE|WS_CHILD, 180, yPos, 50, 20, hWnd, C_PL_SAVE, g_hInst, NULL);}
至此所有组件已经创建完毕,接下来操作这些组件达到控制目的。所有的实现都在WndProc回调函数中。
WndProc函数
控制组件操作
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){PAINTSTRUCT ps;HDC hdc;RECT rect;static RECT oldRect;switch (message) {case WM_PAINT: //重绘消息case WM_COMMAND:……default:return DefWindowProc(hWnd, message, wParam, lParam);}return 0;}
按钮的操作大部分在WM_COMMAND消息中,当各个组件被操作时此处接收到消息,通过LOWORD(wParam)获取组件ID,即如C_BTN_PLAY播放/暂停按钮。
case WM_COMMAND:{int id = LOWORD(wParam);switch (id){case C_BTN_PREV:{char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex--;if (playIndex < 0){playIndex = GetDefaultPlaylistTotalItem();}p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;case C_BTN_PLAY:{if (status != C_BTN_PLAY ){mpxPlay();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");}else if (status == C_BTN_PLAY){mpxPause();status = C_BTN_PAUSE;SetWindowText(btnPlay, "play");}}break;case C_BTN_STOP:{if (status == C_BTN_PLAY || status == C_BTN_PAUSE){mpxStop();status = C_BTN_STOP;SetWindowText(btnPlay, "play");}}break;case C_BTN_NEXT:{char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex++;if (playIndex > GetDefaultPlaylistTotalItem()){playIndex = 0;}p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;case C_BTN_FROM_PL:{int selid = HIWORD(wParam);switch (selid) { case LBN_DBLCLK:{// Get selected index.int lbItem = (int)SendMessage(playlistLB, LB_GETCURSEL, 0, 0); char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex = lbItem;p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;default:{}break;}}break;case C_PL_ADD:{AddFile(hWnd);}break;case C_PL_ADDDIR:{AddDirFile(hWnd);}break;case C_PL_DEL:{DelFile(hWnd);}break;case C_PL_SAVE:{DefaultPlaylistSave();}break;default:break;}}break;
实现双击播放列表中某一歌曲播放歌曲功能。LOWORD(wParam)的ID为C_BTN_FROM_PL时表示播放列表被操作,而消息HIWORD(wParam)表示操作该播放列表的消息类型,其中LBN_DBLCLK表示双击播放列表。
case C_BTN_FROM_PL:{int selid = HIWORD(wParam);switch (selid) { case LBN_DBLCLK:{// Get selected index.int lbItem = (int)SendMessage(playlistLB, LB_GETCURSEL, 0, 0); char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex = lbItem;p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;default:{}break;}}break;
通过SendMessage(playlistLB, LB_GETCURSEL,0,0)获取被双击的条目索引,该索引与播放列表歌曲一一对应,因此可以通过GetItemFromDefaultPlaylist(playIndex)获取要播放的歌曲路径。调用MakeLyricPathFromSongPath(p, lyricpath);转换歌曲名称为歌词名称并调用LyricInit(lyricpath)初始化歌词,之后调用mpxPlayFile(songpath)开始播放歌曲并创建定时器BeginTimeEvent()检查播放进度。同时设置播放状态及播放按钮状态。
其中,在初始化歌词之前需要销毁之前的歌词,创建定时器之前需要销毁之前的定时器,每一个定时器只用于当前播放歌曲。
playIndex为全局变量,记录播放列表当前播放曲目,播放上一曲或者播放下一曲只需将其自减或者自增然后其他步骤同上所述。
播放上一曲、播放下一曲、暂停、播放及停止功能实现
int id = LOWORD(wParam);switch (id){case C_BTN_PREV:{char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex--;if (playIndex < 0){playIndex = GetDefaultPlaylistTotalItem();}p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;case C_BTN_PLAY:{if (status != C_BTN_PLAY ){mpxPlay();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");}else if (status == C_BTN_PLAY){mpxPause();status = C_BTN_PAUSE;SetWindowText(btnPlay, "play");}}break;case C_BTN_STOP:{if (status == C_BTN_PLAY || status == C_BTN_PAUSE){mpxStop();status = C_BTN_STOP;SetWindowText(btnPlay, "play");}}break;case C_BTN_NEXT:{char *p = NULL;static WCHAR songpath[MAX_PATH] = {0};char lyricpath[MAX_PATH] = {0};memset(songpath, 0, MAX_PATH);playIndex++;if (playIndex > GetDefaultPlaylistTotalItem()){playIndex = 0;}p = GetItemFromDefaultPlaylist(playIndex);if (p == NULL){break;}MakeLyricPathFromSongPath(p, lyricpath);LyricDestroy();LyricInit(lyricpath);MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p, strlen(p), songpath, MAX_PATH);EndTimeEvent();mpxPlayFile(songpath);BeginTimeEvent();status = C_BTN_PLAY;SetWindowText(btnPlay, "pause");SetWindowText(hWnd, p);}break;
播放上一曲、播放下一曲除了歌曲索引与双击播放列表播放歌曲不同其他都一样。
添加、删除歌曲保存歌曲目录实现,LOWORD(wParam) ID为对应的C_PL_ADD等值
case C_PL_ADD:{AddFile(hWnd);}break;case C_PL_ADDDIR:{AddDirFile(hWnd);}break;case C_PL_DEL:{DelFile(hWnd);}break;case C_PL_SAVE:{DefaultPlaylistSave();}break;
分别调用了AddFile、AddDirFile、DelFile、DefaultPlaylistSave函数实现。歌曲列表实现见playlist.c文件。
AddFile函数
添加歌曲功能
int AddFile(HWND hWnd){OPENFILENAME fn;char filefilter[] ="All Supported files\0*.mp1;*.mp2;*.mp3;*.m3u;*.ogg;*.pls;*.wav\0MPEG audio files (*.mp1;*.mp2;*.mp3)\0*.mp1;*.mp2;*.mp3\0Vorbis files (*.ogg)\0Playlist files (*.m3u;*.pls)\0*.m3u;*.pls\0WAV files (*.wav)\0*.wav\0All Files (*.*)\0*.*\0";BOOL retVal = FALSE;char initialfilename[MAX_PATH * 100] = "";fn.lStructSize = sizeof(OPENFILENAME);fn.hwndOwner = hWnd;fn.hInstance = NULL;fn.lpstrFilter = filefilter;fn.lpstrCustomFilter = NULL;fn.nMaxCustFilter = 0;fn.nFilterIndex = 0;fn.lpstrFile = initialfilename;fn.nMaxFile = MAX_PATH * 200;fn.lpstrFileTitle = NULL;fn.nMaxFileTitle = 0;fn.lpstrInitialDir = "./";fn.lpstrTitle = NULL;fn.Flags =OFN_ALLOWMULTISELECT | OFN_HIDEREADONLY | OFN_EXPLORER |OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_ENABLESIZING;fn.nFileOffset = 0;fn.nFileExtension = 0;fn.lpstrDefExt = NULL;fn.lCustData = 0;fn.lpfnHook = NULL;fn.lpTemplateName = NULL;retVal = GetOpenFileName(&fn);if (retVal != FALSE){char path_buffer[MAX_PATH];char *pval = NULL;strcpy(path_buffer, fn.lpstrFile);pval = strrchr(path_buffer, '\\');if (pval == NULL){pval = path_buffer;}else{pval++;}SendMessage(playlistLB, LB_INSERTSTRING, (-1), pval);DefaultPlaylistAddItem(path_buffer);}return 0;}
GetOpenFileName函数获取添加的文件路径,使用SendMessage(playlistLB,LB_INSERTSTRING, (-1), pval)将歌曲名称发送到播放列表playlistBL组件,并调用DefaultPlaylistAddItem(path_buffer);函数将歌曲路径添加到默认播放列表中。
ps:SendMessage消息函数详见MSDN,其中LB_INSERTSTRING为listbox组件插入到末尾的消息。
AddDirFile函数
添加整个文件夹歌曲
int AddDirFile(HWND hWnd){BROWSEINFO browseinfo;LPITEMIDLIST itemlist;int image = 0;char directorychoice[MAX_PATH];char fullpath[MAX_PATH];HANDLE found;WIN32_FIND_DATA finddata;char pathbuf2[MAX_PATH];chardirBuf[MAX_PATH];browseinfo.hwndOwner = hWnd;browseinfo.pidlRoot = NULL;browseinfo.pszDisplayName = directorychoice;browseinfo.lpszTitle = "Choose a directory to add";browseinfo.ulFlags = BIF_EDITBOX;browseinfo.lpfn = NULL;browseinfo.lParam = 0;browseinfo.iImage = image;itemlist = SHBrowseForFolder(&browseinfo);if (itemlist == NULL){return 1;}SHGetPathFromIDList(itemlist,dirBuf);if (dirBuf[strlen(dirBuf) - 1] == '\\'&& strcmp(dirBuf, "\\") != 0) dirBuf[strlen(dirBuf) - 1] ='\0';strcpy(fullpath, dirBuf);if (strcmp(fullpath, "\\") == 0)strcat(fullpath, ".\\*.mp3");elsestrcat(fullpath, "\\*.mp3");found = FindFirstFile(fullpath, &finddata);do {char somepath[MAX_PATH];strcpy(somepath, dirBuf);if (strcmp(somepath, "\\") == 0)strcpy(somepath, "\\.");sprintf(pathbuf2, "%s\\%s", somepath, finddata.cFileName);if ((finddata.cFileName[0] != '.' && finddata.cFileName[1] != 0)&& (finddata.cFileName[0] != '.'&& finddata.cFileName[1] != '.'&& finddata.cFileName[2] != 0)){char *pval = NULL;pval = strrchr(pathbuf2, '\\');if (pval == NULL){pval = pathbuf2;}else{pval++;}SendMessage(playlistLB, LB_INSERTSTRING, (-1), pval);DefaultPlaylistAddItem(pathbuf2);}}while (FindNextFile(found, &finddata));FindClose(found);return 0;}
DelFile函数
删除歌曲功能
int DelFile(HWND hWnd){// Get selected index.int lbItem = -1;lbItem = (int)SendMessage(playlistLB, LB_GETCURSEL, 0, 0);if (lbItem != -1){DefaultPlaylistDeleteItem(lbItem);SendMessage(playlistLB, LB_DELETESTRING, lbItem, 0);}return 0;}
调用SendMessage(playlistLB, LB_GETCURSEL,0,0)获取当前选中的歌曲索引,并调用DefaultPlaylistDeleteItem(lbItem);SendMessage(playlistLB,LB_DELETESTRING, lbItem,0);分别删除播放列表中的歌曲和播放列表组件中项。
下一篇音乐播放器播放列表实现解析
- 音乐播放器之源码解析三
- 音乐播放器之源码解析一
- 音乐播放器之源码解析二
- 音乐播放器之源码解析四
- 音乐播放器之源码解析五
- 音乐播放器之源码解析六
- android源码解析------Music 音乐播放器
- 解析service(三):音乐播放器
- 音乐播放器源码
- android-音乐播放器实现及源码下载(三)
- android-音乐播放器实现及源码下载(三)
- android音乐播放器源码
- 音乐播放器源码下载
- android音乐播放器源码
- 小程序(之音乐播放器源码实现)
- 媒体播放之MediaPlayer|三种媒体源|音乐播放器源码|服务中使用|前台运行|处理音频|检索本地歌曲焦点
- android之音乐播放器
- html之音乐播放器
- mac 下 android 开发环境搭建 cocos2d-x
- 每天学习一点.net(5) 利用匿名方法Lambda表达式(=>)实现委托
- 常见数据库默认的端口号
- Oracle创建表空间、创建用户、授权、授权对象的访问以及查看权限集合
- javascript判断一个字符串中出现次数最多的字符,统计这个次数
- 音乐播放器之源码解析三
- SWING中组件的使用
- 2013年5月工作小结 -- 需求变更与Bug
- OnClick与OnClientClick的时序和条件
- Linux makefile 教程 非常详细,且易懂
- 算法题---链表篇
- 使用WebSocket推送服务器消息
- String分割成数组后,判断数组中是否有重复数据,有抛异常
- XP软件限制组策略、注册表限制攻与防 及如何禁止别的用户在自己电脑上安装软件程序