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

自動循環播放 MP3 播放器開發實戰 (下):完整定時器邏輯、自訂繪製 UI 與配置持久化

     編輯:VC++

承上篇:完整定時器驅動的狀態機

本篇補全 OnTimer 核心邏輯、自訂繪製對話框基類、配置檔讀寫與目錄掃描實作,構建生產級自動播放系統。

完整 OnTimer 狀態機實作


void CMp3PlayerDlg::OnTimer(UINT nIDEvent)
{
    // ---- ID 1: 排程檢查 (每秒) ----
    if (nIDEvent == 1)
    {
        Init(); // 重置狀態旗標
        if (!m_bAutoRun) return;
        if (!m_ConfigFile.Open()) return;
        
        m_ConfigFile.ReadSetInfo(m_SetInfo); // 全域設定 (循環開關等)
        SYSTEMTIME st; GetLocalTime(&st);
        m_ConfigFile.ReadDayInfo(st.wDayOfWeek, m_DayInfo); // 當天時段配置
        m_ConfigFile.Close();
        
        int hour, min;
        
        // 早班時段
        hour = GetHour(m_DayInfo.sSStartTime); min = GetMin(m_DayInfo.sSStartTime);
        if (st.wHour == hour && st.wMinute == min)
        {
            LoadCatalogMusic(m_DayInfo.sSMusic);
            m_nPlay = 0; Play(0);
            KillTimer(1);
            SetTimer(3, _ttoi(m_DayInfo.sSDuration) * 60000, NULL);
            m_nFlag = 1; return;
        }
        
        // 中班時段
        hour = GetHour(m_DayInfo.sMStartTime); min = GetMin(m_DayInfo.sMStartTime);
        if (st.wHour == hour && st.wMinute == min)
        {
            LoadCatalogMusic(m_DayInfo.sMMusic);
            m_nPlay = 0; Play(0);
            KillTimer(1);
            SetTimer(3, _ttoi(m_DayInfo.sMDuration) * 60000, NULL);
            m_nFlag = 2; return;
        }
        
        // 晚班時段
        hour = GetHour(m_DayInfo.sEStartTime); min = GetMin(m_DayInfo.sEStartTime);
        if (st.wHour == hour && st.wMinute == min)
        {
            LoadCatalogMusic(m_DayInfo.sEMusic);
            m_nPlay = 0; Play(0);
            KillTimer(1);
            SetTimer(3, _ttoi(m_DayInfo.sEDuration) * 60000, NULL);
            m_nFlag = 3; return;
        }
    }
    
    // ---- ID 3: 時段結束 (一次性) ----
    if (nIDEvent == 3)
    {
        if (m_Music.IsOpen())
        {
            m_Music.Close();
            SetTimer(1, 1000, NULL); // 重新啟動排程檢查
            KillTimer(2); KillTimer(3);
        }
        return;
    }
    
    // ---- ID 4: 啟動自動隱藏 (一次性) ----
    if (nIDEvent == 4)
    {
        ShowWindow(SW_HIDE);
        KillTimer(4);
        return;
    }
    
    // ---- ID 2: 播放進度更新 (每秒) ----
    if (nIDEvent == 2 && m_Music.IsOpen() && !m_bPause)
    {
        m_nPos++;
        m_sMusicPos.Format(_T("%d分%d秒"), m_nPos / 60, m_nPos % 60);
        UpdateData(FALSE);
        m_MPBar.SetScrollPos(m_nPos);
        
        // 單曲結束判斷
        if (m_nPos >= m_nTime)
        {
            if (m_nPlay == m_MusicAry.GetSize() - 1) // 最後一首
            {
                if ((m_bAutoRun && m_SetInfo.bCycle) || !m_bAutoRun) // 循環條件
                {
                    m_nPlay = 0; Play(0); m_MPBar.SetScrollPos(0);
                }
                else // 結束播放
                {
                    m_Music.Close();
                    SetTimer(1, 1000, NULL);
                    KillTimer(2); KillTimer(3);
                    return;
                }
            }
            else // 下一首
            {
                Play(++m_nPlay);
                m_MPBar.SetScrollPos(0);
            }
        }
    }
    
    COwnerDrawDialog::OnTimer(nIDEvent);
}

自訂繪製對話框基類:COwnerDrawDialog

實現非矩形視窗、漸層標題列、自繪按鈕、透明文字等視覺效果:


// COwnerDrawDialog.h
class COwnerDrawDialog : public CDialog
{
    DECLARE_DYNAMIC(COwnerDrawDialog)
public:
    struct DLG_STYLE
    {
        CString sTitleName;
        COLORREF crTitleText;
        COLORREF crTitleStart, crTitleEnd; // 漸層
        BOOL bRoundCorner;
        int nCornerRadius;
    } m_DlgStyle;
    
    COwnerDrawDialog(UINT nIDTemplate, CWnd* pParent = NULL);
    virtual ~COwnerDrawDialog();
    
protected:
    virtual void OnPaint();           // 繪製背景、標題列、圓角
    virtual void DrawTitleBar(CDC* pDC, CRect rect);
    virtual void DrawRoundRect(CDC* pDC, CRect rect, int radius);
    virtual BOOL OnEraseBkgnd(CDC* pDC); // 雙緩衝防閃爍
    virtual LRESULT OnNcHitTest(CPoint point); // 拖曳移動支援
    DECLARE_MESSAGE_MAP()
};

// 繪製關鍵邏輯
void COwnerDrawDialog::OnPaint()
{
    CPaintDC dc(this);
    CMemDC memDC(&dc); // 雙緩衝
    
    CRect rect; GetClientRect(&rect);
    
    // 1. 背景漸層
    DrawGradientRect(&memDC, rect, m_DlgStyle.crTitleStart, m_DlgStyle.crTitleEnd, TRUE);
    
    // 2. 標題列區域
    CRect titleRect = rect; titleRect.bottom = 30;
    DrawTitleBar(&memDC, titleRect);
    
    // 3. 圓角裁剪 (WS_EX_LAYERED + UpdateLayeredWindow 更佳)
    if (m_DlgStyle.bRoundCorner)
    {
        CRgn rgn; rgn.CreateRoundRectRgn(0, 0, rect.Width()+1, rect.Height()+1, 
                                         m_DlgStyle.nCornerRadius, m_DlgStyle.nCornerRadius);
        SetWindowRgn(rgn, TRUE);
    }
}

void COwnerDrawDialog::DrawTitleBar(CDC* pDC, CRect rect)
{
    // 漸層背景
    DrawGradientRect(pDC, rect, m_DlgStyle.crTitleStart, m_DlgStyle.crTitleEnd, TRUE);
    
    // 文字
    pDC->SetTextColor(m_DlgStyle.crTitleText);
    pDC->SetBkMode(TRANSPARENT);
    CFont font; font.CreatePointFont(120, _T("微軟正黑體"));
    CFont* pOld = pDC->SelectObject(&font);
    pDC->DrawText(m_DlgStyle.sTitleName, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    pDC->SelectObject(pOld);
}

自繪按鈕:CEllipseButton (橢圓/圓角矩形)


class CEllipseButton : public CButton
{
    DECLARE_DYNAMIC(CEllipseButton)
public:
    COLORREF m_crFaceStart, m_crFaceEnd; // 漸層
    COLORREF m_crText;
    BOOL m_bRoundRect;
    int m_nRadius;
    
    CEllipseButton();
    virtual ~CEllipseButton();
    
protected:
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
    DECLARE_MESSAGE_MAP()
};

void CEllipseButton::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
    CDC* pDC = CDC::FromHandle(lpDIS->hDC);
    CRect rect = lpDIS->rcItem;
    UINT state = lpDIS->itemState;
    
    // 1. 背景漸層
    COLORREF start = (state & ODS_SELECTED) ? m_crFaceEnd : m_crFaceStart;
    COLORREF end   = (state & ODS_SELECTED) ? m_crFaceStart : m_crFaceEnd;
    DrawGradientRect(pDC, rect, start, end, FALSE);
    
    // 2. 邊框
    CPen pen(PS_SOLID, 1, (state & ODS_FOCUS) ? RGB(0,0,255) : RGB(128,128,128));
    CPen* pOldPen = pDC->SelectObject(&pen);
    CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
    
    if (m_bRoundRect)
        pDC->RoundRect(rect, CPoint(m_nRadius, m_nRadius));
    else
        pDC->Ellipse(rect);
    
    pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush);
    
    // 3. 文字
    pDC->SetTextColor(m_crText);
    pDC->SetBkMode(TRANSPARENT);
    CString text; GetWindowText(text);
    pDC->DrawText(text, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}

配置檔持久化:CConfigFile (INI 格式)


class CConfigFile
{
    CString m_Path; // 完整路徑
public:
    CConfigFile() { m_Path = GetAppPath() + _T("MusicPlayer.ini"); }
    
    BOOL Open() { return PathFileExists(m_Path); }
    void Close() {} // INI 無需關閉
    
    // 全域設定
    struct SET_INFO { BOOL bCycle; /* ... */ } m_SetInfo;
    void ReadSetInfo(SET_INFO& info)
    {
        info.bCycle = GetPrivateProfileInt(_T("Global"), _T("Cycle"), 1, m_Path);
    }
    void WriteSetInfo(const SET_INFO& info)
    {
        WritePrivateProfileString(_T("Global"), _T("Cycle"), info.bCycle ? _T("1") : _T("0"), m_Path);
    }
    
    // 每日時段設定
    struct DAY_INFO 
    {
        CString sSStartTime, sSDuration, sSMusic; // 早班
        CString sMStartTime, sMDuration, sMMusic; // 中班
        CString sEStartTime, sEDuration, sEMusic; // 晚班
    };
    
    void ReadDayInfo(int dayOfWeek, DAY_INFO& info) // 0=Sun..6=Sat
    {
        CString section; section.Format(_T("Day%d"), dayOfWeek);
        info.sSStartTime = ReadString(section, _T("SStart"), _T("08:00"));
        info.sSDuration  = ReadString(section, _T("SDuration"), _T("60"));
        info.sSMusic     = ReadString(section, _T("SMusic"), _T(""));
        // ... 中班、晚班同理
    }
    
    void WriteDayInfo(int dayOfWeek, const DAY_INFO& info)
    {
        CString section; section.Format(_T("Day%d"), dayOfWeek);
        WriteString(section, _T("SStart"), info.sSStartTime);
        WriteString(section, _T("SDuration"), info.sSDuration);
        WriteString(section, _T("SMusic"), info.sSMusic);
        // ...
    }
    
private:
    CString ReadString(LPCTSTR section, LPCTSTR key, LPCTSTR def)
    {
        TCHAR buf[260]; GetPrivateProfileString(section, key, def, buf, 260, m_Path); return buf;
    }
    void WriteString(LPCTSTR section, LPCTSTR key, LPCTSTR val)
    { WritePrivateProfileString(section, key, val, m_Path); }
    
    CString GetAppPath()
    {
        TCHAR path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH);
        PathRemoveFileSpec(path); PathAddBackslash(path); return path;
    }
};

目錄掃描載入播放清單:LoadCatalogMusic


void CMp3PlayerDlg::LoadCatalogMusic(LPCTSTR dirPath)
{
    m_MusicList.ResetContent();
    m_MusicAry.RemoveAll();
    
    CString searchPath = dirPath;
    PathAddBackslash(searchPath.GetBuffer()); searchPath.ReleaseBuffer();
    searchPath += _T("*.*");
    
    CFileFind finder;
    BOOL found = finder.FindFile(searchPath);
    while (found)
    {
        found = finder.FindNextFile();
        if (finder.IsDots() || finder.IsDirectory()) continue;
        
        CString ext = finder.GetFileName();
        int dot = ext.ReverseFind('.');
        if (dot != -1)
        {
            ext = ext.Mid(dot + 1).MakeLower();
            // 支援副檔名清單
            if (ext == _T("mp3") || ext == _T("wav") || ext == _T("wma") || 
                ext == _T("mid") || ext == _T("midi") || ext == _T("ogg") ||
                ext == _T("flac") || ext == _T("aac") || ext == _T("m4a"))
            {
                CString fullPath = finder.GetFilePath();
                m_MusicAry.Add(fullPath);
                m_MusicList.AddString(finder.GetFileName());
            }
        }
    }
    finder.Close();
    
    if (m_MusicAry.GetSize() > 0) m_MusicList.SetCurSel(0);
}

音量控制 MCI 封裝


void CMp3PlayerDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
    if (pScrollBar == &m_Volume)
    {
        m_nVolume = m_Volume.GetScrollPos();
        // MCI setaudio 命令: volume 範圍 0-1000
        CString cmd; cmd.Format(_T("setaudio %s volume to %d"), m_Music.GetAlias(), m_nVolume);
        mciSendString(cmd, NULL, 0, NULL);
    }
    COwnerDrawDialog::OnVScroll(nSBCode, nPos, pScrollBar);
}

部署與啟動項註冊


void RegisterAutoStart(BOOL bEnable)
{
    HKEY hKey;
    RegCreateKeyEx(HKEY_CURRENT_USER, 
        _T("Software\\Microsoft\\Windows\\CurrentVersion\\Run"), 
        0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
    
    if (bEnable)
    {
        TCHAR path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH);
        RegSetValueEx(hKey, _T("AutoMusicPlayer"), 0, REG_SZ, (BYTE*)path, (lstrlen(path)+1)*sizeof(TCHAR));
    }
    else
    {
        RegDeleteValue(hKey, _T("AutoMusicPlayer"));
    }
    RegCloseKey(hKey);
}

專案總結與擴充建議

模組關鍵技術擴充方向
音訊引擎MCI (CCOMMCI)遷移至 IMFMediaEngine (Media Foundation) 或 BASS/FMOD 獲取更好格式支援/均衡器/視覺化
排程引擎多 SetTimer 狀態機改用 Task Scheduler API 或 Quartz.NET 系統級排程,支援節假日/例外日
配置存儲INI (GetPrivateProfileString)XML/JSON + CMarkup/rapidjson,支援多配置集/匯入匯出
UI 框架MFC + COwnerDrawDialog遷移至 WPF/WinUI 3 獲得現代化設計、DPI 感知、動畫、觸控支援
部署模式註冊表 Run 鍵自啟MSIX 封裝、啟動工作、系統服務 (無 UI 背景播放)

結語

從 MCI 基礎播放、定時器狀態機排程、自訂繪製視覺化、INI 配置持久化到目錄遞迴掃描,本專案完整展示了傳統 MFC 多媒體應用的開發全貌。雖然技術棧屬經典範疇,但模組解耦、狀態機設計、資源管理 (RAII)、訊息驅動架構等軟體工程原則放諸四海皆準。若今日重寫,建議採用 .NET 6+ / WPF / Media Foundation / System.Threading.Channels / Microsoft.Extensions.Hosting 技術棧,將業務邏輯與 UI 徹底分離,構建可測試、可維護、可雲端部署的現代化背景音樂服務。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved