本篇補全 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.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);
}
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);
}
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;
}
};
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);
}
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 徹底分離,構建可測試、可維護、可雲端部署的現代化背景音樂服務。