Introduction
When creating dialogs (i.e. a composite ActiveX control) using ATL it is sometimes
necessary to change
the behaviour of the embedded Windows controls. It could, for instance, be changing the behaviour of the
RETURN
key to act as the TAB
key. It could also be only allowing specific keystrokes. When using MFC this is a fairly easy task.
The following will show how to do this using a subclass for a WTL class that can do
the task. The advantage of using WTL to do this is to reuse the work done by Microsoft in
implementing all the messages as methods. The following example subclasses an edit
control but the same techniques can be used with many other controls, like a button control.
The task can be divided into four subtasks
- Create a subclass (in OO terms) of a WTL control (here an edit control) that does the job
- Add the control to the project
- Instantiate it at runtime and attach it to the control ("Windows SubClass"-it)
- Test, debug and test
IMPORTANT: You will need to have the platform SDK installed and set an include path to
the "Microsoft Platform SDK\Src\WTL\Include" directory to be able to use WTL classes and run the demo.
Task 1: Creating the subclass
Just simply creating a subclass from the WTL class
CEditT
(
CEdit
)
is not what we want because
that will only make it possible to change sending of Windows messages to the control
from us into C++-method calls. That is not sufficient. We would also like to react on
messages sent to the control from the Windows system. By subclassing from
CWindowImpl
(from atlwin.h) instead we will get a message map and that is half the job. The
definition of the class could look like:
template < class T>
class CEditEnterAsTabT : public CWindowImpl< CEditEnterAsTabT< T > ,CEdit>
{
public:
BEGIN_MSG_MAP(CEditEnterAsTabT< T >)
MESSAGE_HANDLER(WM_CHAR, OnChar)
MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown)
END_MSG_MAP()
CEditEnterAsTabT(HWND hWnd = NULL){ }
CEditEnterAsTabT< T >& operator=(HWND hWnd);
LRESULT OnChar(UINT uMsg, WPARAM wParam,LPARAM lParam,
BOOL& bHandled);
LRESULT OnKeyDown(UINT uMsg, WPARAM wParam,LPARAM lParam, BOOL& bHandled)
BOOL AttachToDlgItem(HWND parent, UINT dlgID)
}
The hard part is to get the right parameters to the CWindowImpl
template.
The first is the class itself (here CEditEnterAsTabT
) and the seconds is a
CWindow
look-alike-class (here CEdit
which is the same as CEditT< CWindow >
)
As seen the message map contains two messages. The messages we do not handle might be
handled by the superclasses of this control. We add message handlers for these as we
would have done in any MFC program except that we have to do the message cracking
our-selves.
This class definition gives the following inheritance graph:
As seen the new class CEditEnterAsTabT
is both a CEdit
class,
a CWindow
class, and a CMessageMap
class.
In this example we need to implement handlers for both the
WM_KEY
and
WM_CHAR
. Actually the
WM_KEY
should be enough, but if we
don't implement the handler for
WM_CHAR
and ignore the carriage return
we will get a beep. The code looks like:
LRESULT OnChar(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
switch (wParam)
{
case '\r':
return 0;
break;
}
return DefWindowProc(uMsg, wParam, lParam);
}
LRESULT OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
switch (wParam)
{
case VK_RETURN:
case VK_TAB:
::PostMessage (m_parent, WM_NEXTDLGCTL, 0, 0L);
return FALSE;
}
return DefWindowProc(uMsg, wParam, lParam);
}
As seen the control will call the
DefWindowProc
if it doesn't want to
catch the key or character.
Task 2: Adding the control
Include the new class in the project and make as many instances as wanted in the composite
control. I suggest they are private members of the composite control class, like:
private:
WTL::CEditEnterAsTab m_editEnterAsTab1;
WTL::CEditEnterAsTab m_editEnterAsTab2;
Task 3: Instantiate and Attaching
The dynamic attachment of the windows control to our class is usually done in the handler
for
WM_INITDIALOG
, i.e.
OnInitDialog
. This handler can be made
by rightclicking on the
composite control class, choosing "Add Windows Message Handler", and then choosing
WM_INITDIALOG
. When we want to use our message map before the controls own
message
handler we need to attach our own windows-procedure. We don't need to implement
procedure by ourselves. It is already done. We only need to attach it. This is done by
calling the member method
SubclassWindow
. I have created a method
(
AttachToDlgItem
) that
will do this for us from the dialog item ID. It looks like:
BOOL AttachToDlgItem(HWND parent, UINT dlgID)
{
m_dlgItem = dlgID;
m_parent = parent;
m_hWnd = ::GetDlgItem(parent,dlgID);
return SubclassWindow(m_hWnd);
}
The
OnInitDialog
handler could now look like:
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
m_editEnterAsTab1.AttachToDlgItem(this->m_hWnd,IDC_EDITSUBCLASSED1);
m_editEnterAsTab2.AttachToDlgItem(this->m_hWnd,IDC_EDITSUBCLASSED2);
return 0;
}
Your new edit controls message map will now be executed before the windows control gets its
chance.
Task 4: Test, Debug, and Test
You can now insert breakpoints in you code and see what happens. To test the demo project
you must compile the demo project, use the ActiveX Control Test Container and insert the
SubClassEditCtrlContainer
ActiveX control. The behaviour should be that the
ENTER
key
works as a
TAB
key for the first and third Edit-control.