首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC++

自動循環播放 MP3 播放器開發實戰 (上):核心架構、多檔案選擇與進度條實作

     編輯:VC++

專案需求:企業文化背景音樂自動化系統

公司需在上班 (早)、用餐 (中)、下班 (晚) 三個時段各播放 1 小時背景音樂,要求:

  • 啟動後常駐托盤,無需人工干預。
  • 支援多目錄配置,時段到達自動載入播放清單。
  • 單曲結束自動切換下一曲,清單結束循環播放。
  • 時段結束自動停止,等待下一時段。
  • 提供進度條、音量控制、暫停/繼續等手動介面。

技術棧與核心元件

功能技術/類別說明
音訊播放MCI (Media Control Interface) / CCOMMCI 封裝類Windows 多媒體標準 API,支援 MP3/WAV/MID 等
定時排程多 SetTimer (ID 1,2,3,4)ID1: 時段檢查(1秒) ID2: 進度更新(1秒) ID3: 時段結束 ID4: 啟動隱藏
檔案選擇CFileDialog 多選 + 多副檔名過濾OFN_ALLOWMULTISELECT
目錄選擇SHBrowseForFolder / BROWSEINFO配置時段音樂目錄
托盤常駐Shell_NotifyIcon (NIM_ADD) + WS_EX_TOOLWINDOW隱藏工作列按鈕,僅顯示托盤圖示
進度條CScrollBar 模擬 / CSliderCtrl範圍 0~總秒數,拖曳 Seek
自訂繪製COwnerDrawDialog 基類橢圓按鈕、彩色標題列、漸層背景

核心程式碼解析

1. 多檔案開啟對話框與過濾器建構


void CMp3PlayerDlg::OnOpen()
{
    m_MusicList.ResetContent();
    m_MusicAry.RemoveAll();
    m_Music.Close(); // 關閉當前播放
    Init();
    m_Pause.SetWindowText(_T("暫停"));
    
    // 建構多群組過濾器字串
    CString filter = _T("媒體檔案 (所有類型)|*.cda;*.mid;*.rmi;*.midi;*.asf;*.wm;*.wma;*.asx;*.wax;*.m3u;*.wvx;")
                     _T("*.mpeg;*.mpg;*.m1v;*.mp3;*.avi;*.wmv;*.wav;*.snd;*.au;*.aif;*.aifc|");
    filter += _T("CD 音軌 (*.cda)|*.cda|")
              _T("MIDI 檔案 (*.mid;*.rmi;*.midi)|*.mid;*.rmi;*.midi|")
              _T("Windows Media (*.asf;*.wm;*.wma;*.wa?)|*.asf;*.wm;*.wma;|")
              _T("播放清單 (*.asx;*.wax;*.m3u;*.wvx)|*.asx;*.wax;*.m3u;*.wvx|")
              _T("MPEG 影音 (*.mpeg;*.mpg;*.m1v;*.mp3)|*.mpeg;*.mpg;*.m1v;*.mp3|")
              _T("AVI/WMV 影片 (*.avi;*.wmv)|*.avi;*.wmv|")
              _T("音訊檔案 (*.wav;*.snd;*.au;*.aif;*.aifc)|*.wav;*.snd;*.au;*.aif;*.aifc|")
              _T("所有檔案 (*.*)|*.*|");
    
    CFileDialog dlg(TRUE, NULL, NULL, 
                    OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT, 
                    filter, this);
    
    // 重要:多選時需指定足夠緩衝區
    dlg.GetOFN().lpstrFile = new TCHAR[10240];
    dlg.GetOFN().nMaxFile = 10240;
    
    if (dlg.DoModal() == IDOK)
    {
        POSITION pos = dlg.GetStartPosition();
        while (pos != NULL)
        {
            CString path = dlg.GetNextPathName(pos);
            m_MusicAry.Add(path); // 完整路徑陣列
            
            // 列表顯示僅檔名
            int idx = path.ReverseFind('\\');
            CString name = (idx == -1) ? path : path.Mid(idx + 1);
            m_MusicList.AddString(name);
        }
    }
    delete[] dlg.GetOFN().lpstrFile;
}

2. 播放核心邏輯:MCI 開啟/播放/進度同步


void CMp3PlayerDlg::Play(int index)
{
    if (index < 0 || index >= m_MusicAry.GetSize()) return;
    
    m_Music.Close(); // 關閉前一首
    m_MusicList.SetCurSel(index);
    
    CString path = m_MusicAry.GetAt(index);
    m_Music.Open(path); // MCI open command
    
    m_Status.SetWindowText(path + _T(" 正在播放..."));
    
    // 取得總長度 (毫秒)
    m_nLength = m_Music.GetLength(); 
    m_nPos = 0;
    m_nTime = m_nLength / 1000; // 總秒數
    
    // 格式化顯示
    m_sMusicLen.Format(_T("%d分%d秒"), m_nTime / 60, m_nTime % 60);
    m_sMusicPos.Format(_T("%d分%d秒"), 0, 0);
    UpdateData(FALSE);
    
    // 設定進度條範圍
    m_MPBar.SetScrollRange(0, m_nTime);
    m_MPBar.SetScrollPos(0);
    
    // 音量滑桿
    m_Volume.SetRange(0, 1000);
    m_Volume.SetPos(m_nVolume);
    
    // 開始播放
    m_Music.Play(m_hWnd); // MCI play notify -> 視窗接收 MM_MCINOTIFY
    
    // 啟動進度更新定時器 (1 秒)
    SetTimer(2, 1000, NULL);
}

3. 進度條拖曳 Seek 實作


void CMp3PlayerDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
    if (pScrollBar == &m_MPBar)
    {
        switch (nSBCode)
        {
            case SB_THUMBTRACK: // 拖曳中
            case SB_THUMBPOSITION: // 釋放
                m_nPos = nPos;
                m_MPBar.SetScrollPos(m_nPos);
                m_Music.Seek(m_nPos * 1000); // MCI seek (毫秒)
                
                // 若原本暫停,Seek 後自動繼續
                if (m_bPause)
                {
                    m_bPause = FALSE;
                    m_Pause.SetWindowText(_T("暫停"));
                    m_Music.Play(m_hWnd);
                }
                break;
        }
    }
    COwnerDrawDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}

4. 托盤圖示建立與訊息處理


BOOL CMp3PlayerDlg::OnInitDialog()
{
    // ... 基類初始化 ...
    
    // 1. 設定 WS_EX_TOOLWINDOW 隱藏工作列按鈕
    LONG exStyle = GetWindowLong(GetSafeHwnd(), GWL_EXSTYLE);
    SetWindowLong(GetSafeHwnd(), GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW);
    
    // 2. 建立托盤圖示
    NOTIFYICONDATA nid = {0};
    nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.hWnd = m_hWnd;
    nid.uID = IDI_MAINFRAME;
    nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
    nid.uCallbackMessage = WM_MY_TRAY_NOTIFY; // 自訂訊息
    nid.hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_MAINFRAME));
    _tcscpy_s(nid.szTip, _T("企業背景音樂播放器"));
    Shell_NotifyIcon(NIM_ADD, &nid);
    
    // 3. 啟動自動隱藏定時器 (100ms 後隱藏主視窗)
    SetTimer(4, 100, NULL);
    
    // 4. 若啟用自動播放,啟動時段檢查定時器
    if (m_bAutoRun) SetTimer(1, 1000, NULL);
    
    return TRUE;
}

LRESULT CMp3PlayerDlg::OnTrayNotify(WPARAM wParam, LPARAM lParam)
{
    if (wParam != IDI_MAINFRAME) return 0;
    
    if (lParam == WM_LBUTTONDOWN) // 左鍵:顯示/隱藏主視窗
    {
        if (IsWindowVisible()) ShowWindow(SW_HIDE);
        else { ShowWindow(SW_SHOW); SetForegroundWindow(); }
    }
    else if (lParam == WM_RBUTTONDOWN) // 右鍵:彈出選單
    {
        CMenu menu; menu.LoadMenu(IDR_TRAY_MENU);
        CMenu* pPopup = menu.GetSubMenu(0);
        CPoint pt; GetCursorPos(&pt);
        SetForegroundWindow(); // 修正選單無焦點問題
        pPopup->TrackPopupMenu(TPM_LEFTALIGN, pt.x, pt.y, this);
    }
    return 0;
}

定時器架構總覽

ID間隔用途關鍵邏輯
11000 ms時段排程檢查比對系統時間與配置 (早/中/晚 開始時間),符合則載入目錄、播放、啟動 ID3
21000 ms播放進度更新m_nPos++、更新進度條/時間顯示、偵測單曲結束 → 自動下一曲/循環
3配置分鐘 × 60000時段持續時間時間到 → 關閉音訊、殺掉 ID2/ID3、重啟 ID1 等待下一時段
4100 ms (一次性)啟動隱藏主視窗ShowWindow(SW_HIDE)、KillTimer(4)

自動排程核心邏輯 (OnTimer ID=1) 摘要


void CMp3PlayerDlg::OnTimer(UINT nIDEvent)
{
    if (nIDEvent == 1) // 排程檢查
    {
        if (!m_bAutoRun) return;
        if (!m_ConfigFile.Open()) return;
        m_ConfigFile.ReadDayInfo(currentWeekDay, m_DayInfo);
        m_ConfigFile.Close();
        
        SYSTEMTIME st; GetLocalTime(&st);
        
        // 早班時段
        if (st.wHour == MorningStartHour && st.wMinute == MorningStartMin)
        {
            LoadCatalogMusic(m_DayInfo.sSMusic); // 掃描目錄填入 m_MusicAry
            Play(0);
            KillTimer(1);
            SetTimer(3, MorningDurationMin * 60000, NULL);
            m_nFlag = 1; return;
        }
        // 中班、晚班邏輯同理...
    }
    else if (nIDEvent == 2) // 進度更新
    {
        if (m_Music.IsOpen() && !m_bPause)
        {
            m_nPos++;
            UpdateProgressUI();
            if (m_nPos >= m_nTime) // 單曲結束
            {
                if (m_nPlay == m_MusicAry.GetSize() - 1) // 最後一首
                {
                    if (m_bAutoRun && m_CycleEnabled) // 循環模式
                    { m_nPlay = 0; Play(0); }
                    else // 非循環或手動模式
                    { StopAllTimers(); }
                }
                else // 下一首
                { Play(++m_nPlay); }
            }
        }
    }
    else if (nIDEvent == 3) // 時段結束
    {
        if (m_Music.IsOpen()) { m_Music.Close(); StopAllTimers(); SetTimer(1, 1000, NULL); }
    }
    else if (nIDEvent == 4) // 啟動隱藏
    { ShowWindow(SW_HIDE); KillTimer(4); }
    
    COwnerDrawDialog::OnTimer(nIDEvent);
}

下篇預告

下篇將詳細介紹:

  • 配置檔儲存格式 (INI/XML/JSON) 與 CConfigFile 實作。
  • 目錄掃描遞迴載入 (LoadCatalogMusic) 支援子目錄與多媒體副檔名。
  • 自訂繪製對話框 (COwnerDrawDialog):橢圓按鈕、漸層標題列、自繪進度條。
  • 音量控制 MCI setaudio 指令封裝。
  • 安裝部署、註冊表自啟、多語系資源。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved