Customizable Masked Edit Control

Thursday May 12th 2005 by Marius Bancila
Share:

Learn about an edit control with a masked input that prohibits pasting text with disallowed characters.

Introduction

Did you ever need an edit control that was allowed to accept only special characters; for instance, only hexadecimal chars and nothing else? That's not that hard to write, and there are plenty of masked controls on the Web that basically do that one way or another, but most of them have a big flaw: You can paste undesired characters in the control. That was something I could not allow in a recent project that I worked on, and I wanted to share what I did to avoid it with those who need something similar. The control presented in this article is designed to support UNICODE characters.

CSpecialEdit Class Implementation

The way to do this is to derive CEdit and override OnChar(), filtering only the desired chars.

void CSpecialEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
   if(GetKeyState(VK_CONTROL) & 0x80000000)
   {
      switch(nChar)
      {
      case 0x03:
         Copy();
         return;
      case 0x16:
         Paste();
         return;
      case 0x18:
         Cut();
         return;
      case 0x1a:
         Undo();
         return;
      }
   }

   if(!IsCharAllowed(nChar))
   {
      MessageBeep(-1);
      return;
   }

   CEdit::OnChar(nChar, nRepCnt, nFlags);
}

First, you must ensure that the Copy/Cut/Paste/Undo operations done with the keyboard are not also discarded. Then, you check the validity of the input and disregard unwanted characters.

bool CSpecialEdit::IsCharAllowed(TCHAR nChar)
{
   switch(nChar)
   {
   case _T('\b'):
   case 10:
   case 13:
      return true;
   }

   ASSERT(m_formatter != NULL);

   return m_formatter->IsCharAllowed(nChar);
}

The backspace key should be allowed to delete characters and the Enter key to use multi-line edits.

m_formatter is an object of type IFormat, an abstract base class that exposes a IsCharAllowed() method that takes a char as input and returns true if the char is in the allowed set.

To make sure users cannot paste text with prohibited characters, WM_PASTE must be handled:

LRESULT CSpecialEdit::WindowProc(UINT message, WPARAM wParam,
                                 LPARAM lParam)
{
   switch(message)
   {
   case WM_PASTE:
      if(!IsClipboardOK())
      {
         MessageBeep(-1);
         return 0;
      }
   }

   return CEdit::WindowProc(message, wParam, lParam);
}

I have overridden the WindowProc() and checked the Clipboard content when a WM_PASTE is received. If the Clipboard contains text with disallowed characters, the message is not further dispatched and the paste operation is canceled.

bool CSpecialEdit::IsClipboardOK()
{
   bool isOK = true;
   COleDataObject obj;

   if (obj.AttachClipboard())
   {
      HGLOBAL hmem    = NULL;
      TCHAR *pUniText = NULL;
      DWORD dwLen     = 0;
      bool bText      = false;

      if (obj.IsDataAvailable(CF_TEXT))
      {
         hmem = obj.GetGlobalData(CF_TEXT);

         char *pCharText = (char*)::GlobalLock(hmem);
#ifdef UNICODE
         int lenA = strlen(pCharText);
         int lenW = MultiByteToWideChar(CP_ACP, 0, pCharText, lenA,
                                        0, 0);
         if (lenW > 0)
         {
            pUniText = ::SysAllocStringLen(0, lenW);
            MultiByteToWideChar(CP_ACP, 0, pCharText, lenA,
                                pUniText, lenW);
            bText = true;
         }
         else
         {
            ::GlobalUnlock(hmem);
            return false;
         }
#else
         pUniText = pCharText;
#endif
      }
#ifdef UNICODE
      else if(obj.IsDataAvailable(CF_UNICODETEXT))
      {
         hmem = obj.GetGlobalData(CF_UNICODETEXT);

         pUniText = (TCHAR*)::GlobalLock(hmem);
      }
#endif
      if(hmem)
      {
         DWORD dwLen = _tcslen(pUniText);

         for(DWORD i=0; i<dwLen && isOK; i++)
            isOK = IsCharAllowed(pUniText[i]);

         ::GlobalUnlock(hmem);
#ifdef UNICODE
         if(bText)
            ::SysFreeString(pUniText);
#endif
      }
      else
         return false;
   }

   return isOK;
}

The IFormat abstract class is derived by the BaseFormat class that cannot be instantiated because it has protected constructors and a destructor.

class BaseFormat : public IFormat
{
   std::vector<TCHAR> m_listChars;

protected:
   BaseFormat();
   virtual ~BaseFormat();
public:

   void SetAllowedChars(std::vector<TCHAR> chars);
   void SetAllowedChars(LPCTSTR chars, int size);

   virtual bool IsCharAllowed(TCHAR nChar);
};

It provides an overloaded method for setting the allowed char set and implements IsCharAllowed() from IFormat.

The base class is derived by a series of BinaryFormat, OctalFormat, DecimalFormat, and HexFormat classes that defines the appropriate allowed char set. Here is the example for HexFormat.

HexFormat::HexFormat()
{
   LPCTSTR format = _T("0123456789ABCDEFabcdef");
   SetAllowedChars(format, _tcslen(format));
}

In addition, there is a CustomFormat class derived from BaseFormat that can define a custom allowed char set.

class CustomFormat : public BaseFormat
{
public:
   CustomFormat(std::vector<TCHAR> chars);
   CustomFormat(LPCTSTR chars, int size);
   virtual ~CustomFormat();
};

The CSpecialEdit class has a pointer to an IFormat object and provides a method to set this pointer to an IFormat object.

void SetFormatter(IFormat *formatter);

By using this approach, it is very easy to create new specific masks by deriving a BaseFormat class; for instance, create a format mask to allow only uppercase letters.

Using the CSpecialEdit Control

Using the CSpecialEdit is very simple. First, you must include a header in your dialog class header:

#include "SpecialEdit.h"

Then, declare a variable of type CSpecialEdit and also a variable of type IFormat* (the following code is taken from the attached demo project where four special edits are used):

CSpecialEdit m_editBinary;
CSpecialEdit m_editCustom;
CSpecialEdit m_editDecimals;
CSpecialEdit m_editHex;

IFormat *m_pBinaryFormat;
IFormat *m_pDecimalFormat;
IFormat *m_pHexFormat;
IFormat *m_pCustomFormat;

In DoDataExchange(), add a call to DDX_Control for each control:

void CSpecialEditsDlg::DoDataExchange(CDataExchange* pDX)
{
   CDialog::DoDataExchange(pDX);
   //{{AFX_DATA_MAP(CSpecialEditsDlg)
   DDX_Control(pDX, IDC_EDIT_BINARY, m_editBinary);
   DDX_Control(pDX, IDC_EDIT_CUSTOM, m_editCustom);
   DDX_Control(pDX, IDC_EDIT_DECIMALS, m_editDecimals);
   DDX_Control(pDX, IDC_EDIT_HEX, m_editHex);
   //}}AFX_DATA_MAP
}

The only thing left to do is to create instances of IFormat-derived classes and set references to them for the edit controls:

void CSpecialEditsDlg::InitEditControls()
{
   m_pBinaryFormat  = new BinaryFormat;
   m_pDecimalFormat = new DecimalFormat;
   m_pHexFormat     = new HexFormat;

   m_editBinary.SetFormatter(m_pBinaryFormat);

   m_editDecimals.SetFormatter(m_pDecimalFormat);

   m_editHex.SetFormatter(m_pHexFormat);
}

For more information (how to use CustomFormat), see the code of the demo project.

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved