現代 Windows 應用程式常需支援「關閉視窗不退出程式、最小化至系統托盤、托盤圖示右鍵選單操作」等行為模式。本文系統性解析 MFC 托盤程式設計核心技術,包含 CSystemTray 類封裝、系統選單修改、視窗狀態管理、動畫效果與優雅退出機制,提供可直接套用的完整架構。
// 標頭檔中定義唯一訊息 ID
#define WM_USER_TRAY_NOTIFICATION (WM_USER + 0x101)
class CVioletPlayDlg : public CDialog
{
// ...
protected:
CSystemTray m_trayIcon; // 托盤圖示物件
BOOL m_bStartMinimized; // 啟動時最小化旗標
// 訊息對應
afx_msg LONG OnTrayNotification(WPARAM wParam, LPARAM lParam);
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
// ...
};
BOOL CVioletPlayDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 1. 取得系統選單控制代碼
CMenu* pSystemMenu = GetSystemMenu(FALSE);
if (pSystemMenu)
{
// 移除分隔線(第 8 項)與「關於」(第 7 項),索引從 0 開始
pSystemMenu->DeleteMenu(8, MF_BYPOSITION); // 分隔線
pSystemMenu->DeleteMenu(7, MF_BYPOSITION); // 關於
}
// 2. 移除對應的命令處理碼(OnSysCommand 中)
// 原始碼中移除:
// if ((nID & 0xFFF0) == IDM_ABOUTBOX) { CAboutDlg dlgAbout; dlgAbout.DoModal(); }
return TRUE;
}
void CVioletPlayDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == SC_CLOSE)
{
// 隱藏視窗而非銷毀
// 動畫效果需在 stdafx.h 定義 WINVER 0x500
#undef WINVER
#define WINVER 0x500
AnimateWindow(GetSafeHwnd(), 1000, AW_HIDE | AW_BLEND); // 淡出動畫
KillTimer(0); // 清除計時器
ShowWindow(SW_HIDE); // 隱藏視窗
return;
}
CDialog::OnSysCommand(nID, lParam);
}
BOOL CVioletPlayDlg::OnInitDialog()
{
// ... 前述初始化 ...
// 建立托盤圖示
// 參數:父視窗、回調訊息、工具提示文字、圖示控制代碼、右鍵選單資源 ID
m_trayIcon.Create(this, WM_USER_TRAY_NOTIFICATION,
"VioletPlay", m_hIcon, IDR_TUOPAN);
// 判斷是否啟動時最小化至托盤
m_bStartMinimized = GetProfileInt(_T(""), _T("StartMinimized"), FALSE);
if (m_bStartMinimized)
{
PostMessage(WM_CLOSE); // 觸發上述隱藏邏輯
}
return TRUE;
}
LONG CVioletPlayDlg::OnTrayNotification(WPARAM wParam, LPARAM lParam)
{
switch (lParam)
{
case WM_RBUTTONDOWN: // 右鍵點擊:顯示內容選單
{
CMenu oMenu;
if (oMenu.LoadMenu(IDR_TUOPAN))
{
CMenu* pPopup = oMenu.GetSubMenu(0);
ASSERT(pPopup != NULL);
CPoint oPoint;
GetCursorPos(&oPoint);
// 動態更新「顯示/隱藏」選單文字
if (IsWindowVisible())
oMenu.ModifyMenu(IDC_SHOW, MF_STRING, IDC_SHOW, "隱藏(&H)");
else
oMenu.ModifyMenu(IDC_SHOW, MF_STRING, IDC_SHOW, "顯示(&S)");
SetForegroundWindow(); // 關鍵:確保選單獲得焦點
pPopup->TrackPopupMenu(
TPM_LEFTALIGN | TPM_RIGHTBUTTON,
oPoint.x, oPoint.y, this);
}
}
break;
case WM_LBUTTONDOWN: // 左鍵單擊
case WM_LBUTTONDBLCLK: // 左鍵雙擊:還原視窗
OnShow();
break;
}
return 0;
}
// 顯示/隱藏切換
void CVioletPlayDlg::OnShow()
{
if (IsWindowVisible())
{
AnimateWindow(GetSafeHwnd(), 1000, AW_HIDE | AW_BLEND);
ShowWindow(SW_HIDE);
}
else
{
ShowWindow(SW_SHOW);
AnimateWindow(GetSafeHwnd(), 1000, AW_SHOW | AW_BLEND);
SetForegroundWindow();
}
}
// 真正退出程式(托盤選單「退出」)
void CVioletPlayDlg::OnTrayExit()
{
if (AfxMessageBox("確定要完全退出程式嗎?", MB_YESNO) == IDYES)
{
AnimateWindow(GetSafeHwnd(), 1000, AW_HIDE | AW_BLEND);
KillTimer(0);
::PostQuitMessage(0); // 真正結束訊息迴圈
}
}
// 檔案選單「退出」僅隱藏視窗
void CVioletPlayDlg::OnFileExit()
{
if (AfxMessageBox("確定要最小化至托盤嗎?", MB_YESNO) == IDYES)
{
AnimateWindow(GetSafeHwnd(), 1000, AW_HIDE | AW_BLEND);
KillTimer(0);
ShowWindow(SW_HIDE);
}
}
| 技術點 | 關鍵實作 | 注意事項 |
|---|---|---|
| 托盤類別 | CSystemTray 繼承 CWnd,封裝 Shell_NotifyIcon | 需包含 SystemTray.h/cpp,來源可參考 VCKBASE 等開源實作 |
| 動畫效果 | AnimateWindow + AW_BLEND | 需 #define WINVER 0x500 在 stdafx.h 最前方 |
| 選單焦點 | TrackPopupMenu 前呼叫 SetForegroundWindow() | 否則選單可能無法正確接收鍵盤/滑鼠事件 |
| 動態選單 | ModifyMenu 依視窗可見狀態切換文字 | 使用 MF_BYCOMMAND 而非位置索引,避免選單結構變更失效 |
| 雙重退出語意 | 托盤「退出」= 終止進程;標題列/檔案「退出」= 隱藏至托盤 | 務必透過確認對話框區分使用者意圖 |
NIF_INFO 旗標與 Shell_NotifyIcon(NIM_MODIFY) 顯示豐富通知CreateMutex 檢測既有實例,若存在則啟動既有視窗並退出新實例專業級托盤應用程式需精心設計視窗生命週期、使用者互動語意與視覺回饋。本文提供的架構涵蓋系統選單重構、關閉行為重定向、動畫效果、動態右鍵選單與雙重退出機制,可作為 MFC 托盤應用開發的標準範本。實務上建議將托盤邏輯封裝為可重用的 CTrayAppModule 類別,降低主對話框複雜度。