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

VC++ 子類化 Edit 控制項實作日期輸入掩碼:YYYY-MM-DD 格式強制輸入

     編輯:VC 教學

前言

標準 `CEdit` 僅支援簡單的數字/大小寫過濾,無法強制格式化輸入如日期 `YYYY-MM-DD`。透過子類化攔截 `WM_CHAR`、`WM_KEYDOWN` 訊息,可在字元進入編輯框前精確控制:只允許數字、自動插入分隔符 `-`、Backspace/Delete 智慧刪除並還原佔位符。本文提供完整 `CDateEdit` 類別實作,含動態/靜態子類化範例、反子類化清理、輸入驗證邏輯。

一、子類化機制回顧

  • 靜態子類化:對話方塊資源建立時,`DoDataExchange` 中 `DDX_Control(pDX, IDC_EDIT, m_dateEdit)` 自動完成。
  • 動態子類化:執行期呼叫 `m_dateEdit.SubclassDlgItem(IDC_EDIT, this)` 或 `SubclassWindow(hWnd)`。
  • 反子類化:`UnsubclassWindow()` 恢復原視窗程序,之後不可再用該物件操作控制項。

二、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/方向鍵智慧導航,失去焦點時驗證日期合法性。

五、常見陷阱與解決

  • 重複子類化:若已用 `DDX_Control` 再呼叫 `SubclassDlgItem` 會斷言失敗。二擇一。
  • 反子類化後操作:`UnsubclassWindow()` 後 `m_dateEdit` 已與視窗分離,呼叫 `SetFocus()` 等會崩潰。
  • IME 輸入法干擾:中文輸入法可能繞過 `WM_CHAR`。需在 `OnImeComposition` 或 `PreTranslateMessage` 額外過濾,或設定 `ES_IME` 樣式控制。
  • 複製貼上:`WM_PASTE` 需額外處理,建議覆寫 `OnPaste` 驗證剪貼簿文字格式。

六、結語

子類化是 MFC 擴充標準控制項行為的核心技術。`CDateEdit` 展示了如何攔截鍵盤訊息、維護格式化字串、智慧游標導航,形成可復用的日期輸入掩碼控制項。同樣模式可擴充至 IP 位址、電話號碼、信用卡號、MAC 位址等任何固定格式輸入場景,是表單開發的利器。

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