エディットボックスを文字数ではなく byte 数で制限する

エディットボックスの文字数を制限するのはものすごく簡単で、

::SendMessage(hWnd, EM_LIMITTEXT, (WPARAM)length, 0);

こうすれば出来るのだけれども、byte 数で制限を掛けたい場合、Unicode だと標準の機能では対応できないので、エディットボックスを拡張する必要があります。


自分は EN_UPDATE を受け取って、その文字列が制限された byte 数以上になっていた場合は前の状態に戻してやろうと思っていたんですが、ユーザによって貼り付けをされて制限された byte 数を超えた場合、本来であればその制限を超えない最低限の文字が入力されるはずなのに、何も起こらないようになってしまいます。
なので、WM_CHAR と WM_PASTE を拾ってきてごにょごにょして文字数を制限してやるぐらいしか無いです。


ATL の場合、エディットボックスを操作するには CWindow を使うんですけど、CWindow 自体はウインドウハンドルの薄い wrapper なので、メッセージの処理は出来ません。
なので、CWindowImpl<> を継承したクラスを作って、メッセージの処理を書くことになります。


利用側は、そのクラスとエディットボックスを関連づけるために、OnInitDialog() とかの初期化の段階で、

 byteLimitEditBox.SubclassWindow(GetDlgItem(IDC_EDIT1));

って書けば、byteLimitEditBox とエディットボックス(のハンドル)が関連づけられて、byteLimitEditBox にメッセージが送られるようになります。


以下自分の作った ByteLimitEditBox。動作確認はあんまりしてないです。

/*!
 * @brief エディットボックスの入力制限を(文字数ではなく)byte 数で制限するためのクラス
 */
class ByteLimitEditBox : public CWindowImpl<ByteLimitEditBox>
{
private:
    bool m_bSetMaxByteLength;           //!< byte 数の制限が掛けられているか
    int m_maxByteLength;                //!< 制限している byte 数

    typedef CWindowImpl<ByteLimitEditBox> ParentType;
public:
    ByteLimitEditBox()
        : m_bSetMaxByteLength(false), m_maxByteLength(0)
    {
    }

    void SetByteLimitText(int byteLength)
    {
        m_maxByteLength = byteLength;
        m_bSetMaxByteLength = true;
        // 一応文字数の LimitText も掛けておく。
        ::SendMessage(ParentType::m_hWnd, EM_LIMITTEXT, (WPARAM)byteLength, 0);
    }

protected:
// メッセージマップ
BEGIN_MSG_MAP(ByteLimitTextBox)
    MESSAGE_HANDLER(WM_CHAR, OnChar)
    MESSAGE_HANDLER(WM_PASTE, OnPaste)
END_MSG_MAP()

    LRESULT OnChar(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        if (!m_bSetMaxByteLength)
        {
            bHandled = false;
            return 0;
        }

        // Backspace や Tab 等の制御文字の場合は処理しない
        if (::_istcntrl((TCHAR)wParam))
        {
            bHandled = false;
        }
        else
        {
            InsertString(CString((TCHAR)wParam));
        }

        return 0;
    }

    LRESULT OnPaste(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        if (!m_bSetMaxByteLength)
        {
            bHandled = false;
            return 0;
        }

        CString strText;

        // クリップボードにテキストデータが入っているかを調べる
        if (!::IsClipboardFormatAvailable(CF_TEXT))
        {
            return 0;
        }

        // クリップボードのオープン
        if (!::OpenClipboard(NULL))
        {
            return 0;
        }

        // クリップボードからデータを取得し、strTextへコピー
        // CF_TEXT は ANSI テキスト。
        // 変な文字コードを入れられると嫌なので、ANSI で処理する。
        // (Unicode で取得するなら CF_UNICODETEXT を指定する必要がある)
        HANDLE hMem = ::GetClipboardData(CF_TEXT);
        LPSTR pMem = (LPSTR)::GlobalLock(hMem);

        // テキストが取れなかった。
        // ::IsClipboardFormatAvailable() と ::OpenClipboard() の間で
        // クリップボードが更新されたり、::GetClipbordData() をするときに
        // ウインドウがアクティブでない場合は、そういうことが起こりえる。
        if (hMem == NULL || pMem == NULL)
        {
            ::GlobalUnlock(hMem);
            ::CloseClipboard();
            return 0;
        }

        // 例外を投げられるとクリップボードがクローズ出来なくなるので。
        try
        {
            strText = pMem;
        }
        catch(...)
        {
            // クローズしてから例外の再送
            ::GlobalUnlock(hMem);
            ::CloseClipboard();
            throw;
        }

        ::GlobalUnlock(hMem);
        ::CloseClipboard();

        InsertString(strText);

        return 0;
    }

protected:
    void InsertString(const CString& str)
    {
        // エディットボックスのテキストを取得
        CString editText;
        {
            int len = (int)::GetWindowTextLength(ParentType::m_hWnd);
            TCHAR* p = new TCHAR[len + 1];
            ::GetWindowText(ParentType::m_hWnd, p, len + 1);
            try
            {
                editText = CString(p, len);
            }
            catch(...)
            {
                // 解放だけはしてやる。
                delete[] p;
                throw;
            }
            delete[] p;
        }

        // 選択位置を取得
        int start, end;
        {
            DWORD word = (DWORD)::SendMessage(ParentType::m_hWnd, EM_GETSEL, 0, 0);
            start = LOWORD(word);
            end = HIWORD(word);
        }

        // 選択している文字列に対して文字を入力すると、
        // その選択している文字列は消去されるので、
        // (擬似的に)選択した範囲の文字列を消去する。
        editText.Delete(start, end - start);

        // ASCII 文字列での長さを計測
        int textLengthA = CStringA(editText.GetString()).GetLength();

        // str から m_maxByteLength を超えない n 文字を探す
        int n = 0;
        for (n = 0; n < str.GetLength(); n++)
        {
            // str[n] を ASCII 文字列としたときの長さを取得
            int charLengthA = CStringA(str[n]).GetLength();
            // 次に入力する文字によって m_maxByteLength を超えてしまうなら break する。
            if (textLengthA + charLengthA > m_maxByteLength)
            {
                break;
            }
            // ASCII 文字列の長さを更新
            textLengthA += charLengthA;
        }

        // n 文字追加する
        if (n > 0)
        {
            // Undo 出来るかどうかを調べる
            BOOL bCanUndo = (BOOL)::SendMessage(ParentType::m_hWnd, EM_CANUNDO, 0, 0);
            // 文字列を挿入
            ::SendMessage(ParentType::m_hWnd, EM_REPLACESEL, (WPARAM)bCanUndo, (LPARAM)str.Mid(0, n).GetString());
        }
    }
};