標準 `CEdit` 僅支援簡單的數字/大小寫過濾,無法強制格式化輸入如日期 `YYYY-MM-DD`。透過子類化攔截 `WM_CHAR`、`WM_KEYDOWN` 訊息,可在字元進入編輯框前精確控制:只允許數字、自動插入分隔符 `-`、Backspace/Delete 智慧刪除並還原佔位符。本文提供完整 `CDateEdit` 類別實作,含動態/靜態子類化範例、反子類化清理、輸入驗證邏輯。
class CDateEdit : public CEdit
{
DECLARE_DYNAMIC(CDateEdit)
public:
CDateEdit();
virtual ~CDateEdit();
void Initialize(); // 設定初始掩碼 "---- -- --"
protected:
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnKillFocus(); // 失去焦點時驗證完整性
DECLARE_MESSAGE_MAP()
// 輔助:根據游標位置計算下一個可輸入位置(跳過 '-')
int NextInputPos(int pos) const;
int PrevInputPos(int pos) const;
// 格式化字串:確保 '-' 固定在第 4、7 字元(0-based)
CString FormatText(const CString& str) const;
};
void CDateEdit::Initialize()
{
SetLimitText(10); // YYYY-MM-DD = 10 字元
SetWindowText(_T("---- -- --")); // 4+1+2+1+2 = 10,用空格佔位較易處理
// 或用 "____-__-__" 底線佔位
}
BEGIN_MESSAGE_MAP(CDateEdit, CEdit)
ON_WM_CHAR()
ON_WM_KEYDOWN()
ON_WM_KILLFOCUS()
END_MESSAGE_MAP()
void CDateEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// 僅處理可列印字元
if (nChar < VK_SPACE) { CEdit::OnChar(nChar, nRepCnt, nFlags); return; }
// 只允許數字
if (nChar < _T('0') || nChar > _T('9')) return; // 忽略非數字
int nStart, nEnd; GetSel(nStart, nEnd);
CString str; GetWindowText(str);
// 計算實際插入位置(跳過分隔符)
int pos = NextInputPos(nStart);
if (pos < 0 || pos >= 10) return; // 已滿
// 替換該位置字元
str.SetAt(pos, (TCHAR)nChar);
SetWindowText(FormatText(str));
// 移動游標到下一個可輸入位置
SetSel(NextInputPos(pos + 1), NextInputPos(pos + 1));
}
void CDateEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
int nStart, nEnd; GetSel(nStart, nEnd);
CString str; GetWindowText(str);
if (nChar == VK_BACK) // Backspace
{
int pos = PrevInputPos(nStart);
if (pos >= 0)
{
str.SetAt(pos, _T(' ')); // 或 '_'
SetWindowText(FormatText(str));
SetSel(pos, pos);
}
return;
}
else if (nChar == VK_DELETE) // Delete
{
int pos = NextInputPos(nStart);
if (pos >= 0 && pos < 10)
{
str.SetAt(pos, _T(' '));
SetWindowText(FormatText(str));
SetSel(pos, pos);
}
return;
}
else if (nChar == VK_LEFT)
{
SetSel(PrevInputPos(nStart), PrevInputPos(nStart));
return;
}
else if (nChar == VK_RIGHT)
{
SetSel(NextInputPos(nStart), NextInputPos(nStart));
return;
}
CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}
void CDateEdit::OnKillFocus()
{
CEdit::OnKillFocus();
// 可在此做完整日期驗證(如月份 01-12、日期 01-31、閏年)
CString str; GetWindowText(str);
str.Remove(_T(' ')); // 移除佔位符
if (str.GetLength() == 8 && str[4] == _T('-') && str[7] == _T('-'))
{
int y = _ttoi(str.Mid(0,4));
int m = _ttoi(str.Mid(5,2));
int d = _ttoi(str.Mid(8,2));
// 簡易驗證...
if (m < 1 || m > 12 || d < 1 || d > 31)
AfxMessageBox(_T("日期數值超出範圍!"));
}
else
{
// 不完整:可選擇清空或保留
}
}
int CDateEdit::NextInputPos(int pos) const
{
while (pos < 10)
{
if (pos == 4 || pos == 7) pos++; // 跳過 '-'
else break;
}
return (pos < 10) ? pos : -1;
}
int CDateEdit::PrevInputPos(int pos) const
{
while (pos >= 0)
{
if (pos == 4 || pos == 7) pos--; // 跳過 '-'
else break;
}
return (pos >= 0) ? pos : -1;
}
CString CDateEdit::FormatText(const CString& str) const
{
CString s = str;
if (s.GetLength() < 10) s += CString(_T(' '), 10 - s.GetLength());
s.SetAt(4, _T('-'));
s.SetAt(7, _T('-'));
return s;
}
// MyDialog.h
CDateEdit m_dateEdit;
// MyDialog.cpp
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_DATE, m_dateEdit);
}
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_dateEdit.Initialize();
return TRUE;
}
void CMyDialog::OnBtnSubclass()
{
m_dateEdit.SubclassDlgItem(IDC_EDIT_DATE, this);
m_dateEdit.Initialize();
GetDlgItem(IDC_BTN_SUBCLASS)->EnableWindow(FALSE);
GetDlgItem(IDC_BTN_UNSUBCLASS)->EnableWindow(TRUE);
}
void CMyDialog::OnBtnUnsubclass()
{
m_dateEdit.UnsubclassWindow();
GetDlgItem(IDC_BTN_SUBCLASS)->EnableWindow(TRUE);
GetDlgItem(IDC_BTN_UNSUBCLASS)->EnableWindow(FALSE);
}

使用者只能輸入數字,分隔符 `-` 自動固定且不可刪除,游標自動跳過分隔符,Backspace/Delete/方向鍵智慧導航,失去焦點時驗證日期合法性。
子類化是 MFC 擴充標準控制項行為的核心技術。`CDateEdit` 展示了如何攔截鍵盤訊息、維護格式化字串、智慧游標導航,形成可復用的日期輸入掩碼控制項。同樣模式可擴充至 IP 位址、電話號碼、信用卡號、MAC 位址等任何固定格式輸入場景,是表單開發的利器。