프로그래밍

ActiveX 컨트롤에서 키보드 입력 처리하는 법

panpro 2007. 4. 25. 11:46
ActiveX 콘트롤에서 키보드 입력 처리하는 법 프로그래밍 노트

2004/12/08 20:04

http://blog.naver.com/kdsong/120008321045

첨부파일SkdTestA_2004-09-15_21.29.28.zip
Keyboard Handling for an ActiveX Control
Kyung-dong Song
SUMMARY
   이 글에서는 ActiveX 콘트롤과 컨테이너 사이에서 키보드 명령을 처리하는 방법에 대한 제반 사항을 연구하고 그 내용을 정리한다.
BASIS OF KEYBOARD HANDLING
  ActiveX 콘트롤과 그것을 표시하는 MFC 클라이언트 프로그램 사이에서 키보드 명령을 처리하는 것을 이해하기 위해서는 윈도우즈에서 키보드를 처리하는 방법부터 확실하게 이해하는 것이 중요하다. 키보드에서 A를 눌렀다고 하자. 이 경우 WM_KEYDOWN, WM_CHAR, WM_KEYUP의 순서대로 메시지가 발생한다. 이 때 WM_CHAR는 사람이 발생시키는 것이 아니고 메시지 루프의 TranslateMessage 함수에서 발생시킨다.
 
  while(GetMessage(&Message,0,0,0)) {
       TranslateMessage(&Message);       DispatchMessage(&Message);
  }
  위 코드에서 GetMessage는 메시지 큐에서 메시지를 꺼내온 후 이 메시지를 TranslateMessage 함수로 넘겨준다. TranslateMessage 함수는 전달된 메시지가 WM_KEYDOWN인지와 눌려진 키가 문자키인지를 검사해보고 조건이 맞을 경우 WM_CHAR 메시지를 만들어 메시지 큐에 덧붙이는 역할을 한다. 문자 입력이 아닐 경우에는 아무 일도 하지 않으며 DispatchMessage 함수에 의해 WndProc로 보내진다.
  다음으로 이해해야 할 내용은 액셀러레이터이다. 액셀러레이터에 대한 자세한 설명은 [1]을 참고하라. 일반적으로 메뉴가 추가된 프로그램의 경우에는 아래에 표시한 형태의 메시지 루프를 가지는데 앞에서 표시한 메시지 루프와 비교하여 LoadAccelerators와 TranslateAccelerator 함수가 추가되었다.
 
  HACCEL hAccel;
  hAccel=LoadAccelerators(hInstance,MAKEINTRESOURCE(IDR_ACCELERATOR1));
  while(GetMessage(&Message,0,0,0)) {
       if (!TranslateAccelerator(hWnd,hAccel,&Message)) {
           TranslateMessage(&Message);
           DispatchMessage(&Message);
       }
  }
HACCEL LoadAccelerators(HISNTANCE hInstance, LPCTSTR lpTableName);
  이 함수는 리소스로부터 액셀러레이터 테이블을 읽어들인다.

int TranslateAccelerator(HWND hWnd, HACCEL hAccTable, LPMSG lpMsg);
  이 함수는 키보드 메시지를 WM_COMMAND 메시지로 변경해주어 액셀러레이터가 동작할 수 있도록 해 준다. 키보드 명령(가령 Ctrl+A를 누른 경우)도 액셀러레이터이기 이전에 메시지이므로 먼저 WM_KEYDOWN 메시지가 발생할 것이고 그대로 두면 WM_KEYDOWN의 메시지 처리 루틴이 처리해버리게 된다.
  TranslateAccelarator는 키보드 입력값을 읽어 이 키값이 지정한 액셀러레이터 테이블에 있는지 먼저 살펴본 후 존재하는 경우에는 그 키값에 해당하는 WM_COMMAND 메시지를 발생시키고 TRUE를 리턴해버린다. 이렇게 되면 TranslateMessage, DispatchMessage 함수가 실행되지 않고 다음에 WM_COMMAND 메시지가 들어올 때 그것을 처리하게 되므로 키보드 명령이 수행되는 것이다.
ATL KEYBOARD HANDLING
  ATL로 만든 ActiveX 콘트롤이 비주얼 베이직 폼에서 포커스를 갖고 있는 상태에서는 폼의 디폴트 버튼에 포커스가 안간다. 즉 ATL로 만든 콘트롤이 포커스를 갖고 있는 상태에서 엔터키를 치면 디폴트 버튼이 동작안하는 문제점이 생긴다는 것이다. 이런 문제점을 해결하기 위해서는 먼저 ATL로 만든 콘트롤이 컨테이너와 어떤 방식으로 정보를 주고 받는지 이해를 해야한다.
  컨테이너가 콘트롤의 키보드 동작 형태를 알기 위해 호출하는 메서드는 IOleControl::GetControlInfo이다. 이 메서드를 호출해서 콘트롤의 키보드 동작 방식에 관한 정보를 얻은 후에 IOleInPlaceActiveObject::TranslateAccelerator 메서드를 호출하여 키보드 명령을 처리한다. ATL에서는 IOleInPlaceActiveObjectImpl<> 클래스에 이 메서드가 구현되어 있다. 아래에 그 구현 코드를 표시하였다.
template <class T>
class ATL_NO_VTABLE IOleInPlaceActiveObjectImpl :
  public IOleInPlaceActiveObject
{
public:
  .....

  STDMETHOD(TranslateAccelerator)(LPMSG pMsg)
  {
       T* pT = static_cast<T*>(this);
       HRESULT hRet = S_OK;
       if (pT->PreTranslateAccelerator(pMsg, hRet))
           return hRet;       CComPtr<IOleControlSite> spCtlSite;
       hRet = pT->InternalGetSite(IID_IOleControlSite, (void**)&spCtlSite);
       if (SUCCEEDED(hRet))
       {
           if (spCtlSite != NULL)
           {
               DWORD dwKeyMod = 0;
               if (::GetKeyState(VK_SHIFT) < 0)
                   dwKeyMod += 1// KEYMOD_SHIFT
               if (::GetKeyState(VK_CONTROL) < 0)
                   dwKeyMod += 2// KEYMOD_CONTROL
               if (::GetKeyState(VK_MENU) < 0)
                   dwKeyMod += 4// KEYMOD_ALT
               hRet = spCtlSite->TranslateAccelerator(pMsg, dwKeyMod);
           }
           else
               hRet = S_FALSE;
       }
       return (hRet == S_OK) ? S_OK : S_FALSE;
  }
  .....
}
  위 코드를 보면 콘트롤이 제공하는 PreTranslateAccelerator 함수를 호출한 후 그 리턴값이 TRUE면 곧바로 메서드를 종료하고, FALSE면 컨테이너의 TranslateAccelerator 메서드를 호출하도록 작성되어 있다. PreTranslateAccelerator은 CComControlBase 클래스에서 제공하는 함수로 아래에 그 코드를 표시하였다. 구현된 코드는 없고 FALSE만 리턴하도록 만들어져 있다. PreTranslateAccelerator를 오버라이드하여 TRUE를 리턴하게 되면 TranslateAccelerator 메서드의 나머지 코드 부분이 호출되지 않음을 기억해두자.
 
class ATL_NO_VTABLE CComControlBase
{
  ...........
 
  BOOL PreTranslateAccelerator(LPMSG /*pMsg*/, HRESULT& /*hRet*/)
  {
       return FALSE;
  }
 
  ...........
}
  PreTranslateAccelerator 함수는 보통의 경우에는 추가되지 않고, 에디트나 ListView와 같은 콘트롤을 기반으로 하는 ActiveX 콘트롤을 만들 때 Wizard에서 생성한 코드에 자동으로 추가된다.
  가령 위 그림처럼 Edit 콘트롤을 기반으로 해서 만들면 PreTranslateAccelerator를 오버라이드한 함수가 콘트롤 클래스에 자동으로 추가된 것을 확인할 수 있다. 이 콘트롤의 경우는 화살표키를 누르면 TRUE를 리턴하므로 TranslateAccelerator 메서드(혹은 이것을 오버라이드한 코드)가 호출되지 않는데, 이 콘트롤은 에디트 콘트롤을 기반으로 제작됐으므로 화살표키를 누르면 입력된 문자열 사이로 커서를 이동시키는 작업이 이뤄져야 하므로 여기서 해당 메시지를 처리하는 것이 당연하다.
 
  BOOL PreTranslateAccelerator(LPMSG pMsg, HRESULT& hRet)
  {
       if(pMsg->message == WM_KEYDOWN &&
           (pMsg->wParam == VK_LEFT ||
           pMsg->wParam == VK_RIGHT ||
           pMsg->wParam == VK_UP ||
           pMsg->wParam == VK_DOWN))
       {
           hRet = S_FALSE;
           return TRUE;
       }
       //TODO: Add your additional accelerator handling code here
       return FALSE;
  }
  TranslateAccelerator 메서드를 설명하다가 PreTranslateAccelerator 함수 설명으로 빠진 것 같은데 이 둘의 관계를 정확하게 이해하는 것이 중요하다. 구현 클래스에서 PreTranslateAccelerator 함수를 오버라이드하면 이 함수의 리턴값이 FALSE('액셀러레이터가 먼저 처리되지 않았다'로 기억하면 헷갈리지 않는다)면 IOleInPlaceActiveObjectImpl<>::TranslateAccelerator 메서드의 나머지 코드가 실행되고, TRUE('액셀러레이터가 먼저 처리되었다'로 기억)면 호출되지 않는다.
   구현할 때는 TranslateAccelerator 메서드와 PreTranslateAccelerator 함수를 모두 오버라이드할 수 있는데, 앞에서 살펴본 TranslateAccelerator 메서드의 구현코드처럼 PreTranslateAccelerator 함수를 호출하는 코드를 넣어주지 않는다면 당연히 PreTranslateAccelerator 함수가 호출되지 않는다.  PreTranslateAccelerator 함수를 오버라이드해서 처리할 수 있으므로 굳이 TranslateAccelerator 메서드까지 오버라이드할 필요는 없다고 하겠다..
  이 글에서 다루는 예제 콘트롤에서는 두가지 경우를 모두 확인해보기 위해서 SkdTestA1 콘트롤은 TranslateAccelerator 메서드를 오버라이드하고, SkdTestA2은 위저드에 의해 추가된 PreTranslateAccelerator 함수를 수정했다.
MFC KEYBOARD HANDLING
  ActiveX 콘트롤을 표시하는 컨테이너 윈도우는 다양한 환경에서 개발될 수 있겠지만, 내 경우에는 대부분 MFC로 만든 윈도우가 컨테이너 역할을 한다. 이 글에서는 MFC의 경우에만 관심을 국한해서 살펴보겠다.
컨테이너 윈도우에 메시지가 전달되면 메시지를 콘트롤에 보내서 처리를 하고, 콘트롤이 다루지 않는 메시지인 경우에는 컨테이너 윈도우가 해당 메시지를 처리하는 것이 일반적인 구현일 것이다. 이런 요구사항을 처리할 가장 적합한 장소는 PreTranslateMessage함수인데 구현 결과를 아래에 표시했다.
 
BOOL CControlViewerView::PreTranslateMessage(MSG* pMsg)
{
  HRESULT hr = S_FALSE;
  IUnknown* pUnk = m_wnd.GetControlUnknown();
  if (pUnk)
  {
       IOleInPlaceActiveObject* pObject = NULL;
       hr = pUnk->QueryInterface(IID_IOleInPlaceActiveObject,
                                 (void**)&pObject);
       if (SUCCEEDED(hr))
       {
           hr = pObject->TranslateAccelerator(pMsg);
           if (pObject) pObject->Release();
       }
  }

  // PreTranslateMessage return value's meaning:
  // Nonzero if the message was translated and should not be
  // dispatched; zero if the message was not translated and
  // should be dispatched.
  BOOL bReturn = FALSE;
  if (hr == S_FALSE)
       bReturn = CView::PreTranslateMessage(pMsg);
  return bReturn;
}
  컨테이너 윈도우가 MDI 차일드 윈도우인 경우에 여러개의 차일드 윈도우를 표시한 후에 윈도우들을 선택하다 보면 ActiveX 콘트롤로 포커스가 제대로 이동하지 않는 것을 경험하게 되는데, 콘트롤이 에디트 윈도우인 경우에는 캐럿이 아예 표시안되는 문제점이 있다.
  가장 먼저 떠오르는 방법으로, CWnd 클래스의 인스턴스인 m_wnd변수를 이용해서 m_wnd.SetFocus()하면 포커스가 이동할 것 같지만 그렇게 동작하지 않는다. MDI 차일드 윈도우가 포커스를 받으면 ActiveX 콘트롤로부터 IOleWindow 인터페이스를 얻고, 이것으로부터 윈도우 핸들을 얻어서 포커스를 설정해야 제대로 동작한다.
void CControlViewerView::OnSetFocus(CWnd* pOldWnd)
{
  CView::OnSetFocus(pOldWnd);
 
  IUnknown* pUnk = m_wnd.GetControlUnknown();
  if (pUnk)
  {
       IOleWindow* pOleWindow = NULL;
       HRESULT hr = pUnk->QueryInterface(IID_IOleWindow,
           (void**)&pOleWindow);
       if (SUCCEEDED(hr))
       {
           HWND hWnd;
           hr = pOleWindow->GetWindow(&hWnd);
           if (SUCCEEDED(hr)) ::SetFocus(hWnd);
           if (pOleWindow) pOleWindow->Release();
       }
  }

}
EXAMPLE CODE 1
  컨테이너에서 입력한 키보드 명령이 동작하는지를 확인하기 위해 만든 SkdTestA1 콘트롤은 Ctrl+←, Ctrl+→ 명령을 받아들여 "Ctrl+Left", "Ctrl+Right" 문자열을 표시하는 기능을 제공한다. 보다시피 별다른 기능을 제공하는 것은 아니고 콘트롤이 액셀러레이터에 반응하는지를 확인하는 수준이다.
콘트롤의 액셀러레이터를 정보를 등록하고 제거하기에 적당한 위치는 FinalConstruct와 FinalRelease일 것이다.
class ATL_NO_VTABLE CSkdTestA1 : ....
{
  .............
private:
  CComBSTR    m_bstrText;
  HACCEL      m_hAccel;};


HRESULT CSkdTestA1::FinalConstruct()
{
  HINSTANCE hInstance = _Module.GetModuleInstance();
  m_hAccel = LoadAccelerators(hInstance,
       MAKEINTRESOURCE(IDR_ACCELERATOR_A1));  return S_OK;
}

void CSkdTestA1::FinalRelease()
{
  DestroyAcceleratorTable(m_hAccel);}
액셀러레이터는 WM_COMMAND 메시지로 전달되므로 메시지맵에 위 액셀러레이터를 처리할 커맨드 핸들러를 추가한다.
 
BEGIN_MSG_MAP(CSkdTestA1)
  CHAIN_MSG_MAP(CComControl<CSkdTestA1>)
  DEFAULT_REFLECTION_HANDLER()
  COMMAND_ID_HANDLER(ID_CTRL_LEFT, OnCtrlLeftKey)
  COMMAND_ID_HANDLER(ID_CTRL_RIGHT, OnCtrlRightKey)  MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
  MESSAGE_HANDLER(WM_RBUTTONDOWN, OnRButtonDown)
END_MSG_MAP()
커맨드 핸들러 함수의 구현 코드는 아래와 같다.
 
LRESULT CSkdTestA1::OnCtrlLeftKey(WORD wNotifyCode,
                                 WORD wID,
                                 HWND hWndCtl,
                                 BOOL& bHandled)
{
  m_bstrText = _T("Ctrl+Left");
  FireViewChange();
  return 0;
}

LRESULT CSkdTestA1::OnCtrlRightKey(WORD wNotifyCode,
                                  WORD wID,
                                  HWND hWndCtl,
                                  BOOL& bHandled)
{
  m_bstrText = _T("Ctrl+Right");
  FireViewChange();
  return 0;
}
 
STDMETHODIMP CSkdTestA1::TranslateAccelerator(MSG *pMsg)
{
  if ((pMsg->message >= WM_KEYFIRST) && (pMsg->message <= WM_KEYLAST))
  {
       // 탭키나 리턴키를 눌렀으면 컨테이너에게 처리를 맡긴다.
       if ((VK_TAB == pMsg->wParam) || (VK_RETURN == pMsg->wParam))
       {
           CComQIPtr<IOleControlSite, &IID_IOleControlSite>
               spCtrlSite(m_spClientSite);
           if (spCtrlSite)
           {
               DWORD km = 0;
               km |= GetKeyState(VK_SHIFT)   < 0 ? KEYMOD_SHIFT   : 0;
               km |= GetKeyState(VK_CONTROL) < 0 ? KEYMOD_CONTROL : 0;
               km |= GetKeyState(VK_MENU)    < 0 ? KEYMOD_ALT     : 0;

               return spCtrlSite->TranslateAccelerator(pMsg, km);
           }
       }

       // Ctrl+Left, Ctrl+Right 키 조합은 Accelerator로 등록시켰고,
       // 콘트롤에서 처리하기를 원하므로 키값을 확인한 후 WM_COMMAND
       // 메시지를 보내고, 컨테이너가 처리하지 않도록 S_OK를 리턴한다.
       // WM_KEYDOWN, WM_KEYUP 둘 다 적용되므로 화면이 두번 그려지는
       // 문제점이 있어서 WM_KEYDOWN만 적용하도록 조건문에서 메시지를
       // 확인한다.
     if ((WM_KEYDOWN == LOWORD(pMsg->message)) &&
           (GetKeyState(VK_CONTROL) < 0))
       {
           switch (pMsg->wParam)
           {
           case VK_LEFT:
               SendMessage(WM_COMMAND, ID_CTRL_LEFT);
               break;
           case VK_RIGHT:
               SendMessage(WM_COMMAND, ID_CTRL_RIGHT);
               break;
           }
           return S_OK;
       }

  }
  return S_FALSE; 
}
EXAMPLE CODE 2
  SkdTestA2 콘트롤은 에디트 윈도우를 기반으로 해서 만들었다. 간단한 메모장으로 생각하면 된다. 메모장에서 Ctrl+C, Ctrl+V 등의 단축키가 지원이 안되면 메모장의 자격이 없으므로 SkdTestA2 콘트롤에도 단축키를 지원하기 위해 아래와 같이 액셀러레이터를 추가하였다.
  SkdTestA2 콘트롤에서는 에디트 윈도우를 기반으로 만들었으므로 PreTranslateAccelerator 함수가 자동으로 추가된다. 이 함수에서 메시지를 확인하고 필요한 처리를 해주면 된다.
BOOL CSkdTestA2::PreTranslateAccelerator(LPMSG pMsg, HRESULT& hRet)
{
  if(pMsg->message == WM_KEYDOWN &&
       (pMsg->wParam == VK_LEFT ||
       pMsg->wParam == VK_RIGHT ||
       pMsg->wParam == VK_UP ||
       pMsg->wParam == VK_DOWN))
  {
       hRet = S_FALSE;
       return TRUE;
  }

  //TODO: Add your additional accelerator handling code here
  if ((WM_KEYDOWN == LOWORD(pMsg->message)) &&
       (GetKeyState(VK_CONTROL) < 0))
  {
       hRet = S_FALSE;
       switch ((char)pMsg->wParam)
       {
       case 'C':
           SendMessage(WM_COMMAND, ID_EDIT_COPY);
           break;
       case 'X':
           SendMessage(WM_COMMAND, ID_EDIT_CUT);
           break;
       case 'V':
           SendMessage(WM_COMMAND, ID_EDIT_PASTE);
           break;
       case 'A':
           SendMessage(WM_COMMAND, ID_EDIT_SELECT_ALL);
           break;
       case 'Z':
           SendMessage(WM_COMMAND, ID_EDIT_UNDO);
           break;
       }
       return TRUE;
  }

  return FALSE;
}
아래에 액셀러레이터들의 핸들러 함수를 표시하였다. 설명을 덧붙이는건 잔소리가 되겠다.
 
LRESULT CSkdTestA2::OnEditCopy(WORD wNotifyCode, WORD wID,
                              HWND hWndCtl, BOOL& bHandled)
{
  ::SendMessage(m_ctlEdit.m_hWnd, WM_COPY, 0, 0);
  return 0;
}

LRESULT CSkdTestA2::OnEditCut(WORD wNotifyCode, WORD wID,
                             HWND hWndCtl, BOOL& bHandled)
{
  ::SendMessage(m_ctlEdit.m_hWnd, WM_CUT, 0, 0);
  return 0;
}

LRESULT CSkdTestA2::OnEditPaste(WORD wNotifyCode, WORD wID,
                               HWND hWndCtl, BOOL& bHandled)
{
  ::SendMessage(m_ctlEdit.m_hWnd, WM_PASTE, 0, 0);
  return 0;
}

LRESULT CSkdTestA2::OnEditUndo(WORD wNotifyCode, WORD wID,
                              HWND hWndCtl, BOOL& bHandled)
{
  ::SendMessage(m_ctlEdit.m_hWnd, EM_UNDO, 0, 0);
  return 0;
}

LRESULT CSkdTestA2::OnEditSelectAll(WORD wNotifyCode, WORD wID,
                                   HWND hWndCtl, BOOL& bHandled)
{
  ::SendMessage(m_ctlEdit.m_hWnd, EM_SETSEL, 0, -1);
  return 0;
}
REFERENCE
[1] 김상형, "Windows API 정복", 가남사, 2002.
    - pp.83 : TranslateMessage를 설명함.
    - pp.128 : TranslateAccelerator를 설명함.
    - pp.993 : MDI 차일드 윈도우에서 사용되는 TranslateMDISysAccel를 설명함.
                    MDI 처리 방식은 차이가 날 수 밖에 없으므로 여기를 꼭 참고할 것.
[2] Brent Rector, Chris Sells, "ATL Internals", Addison-Wesley, 1999.
    - pp.535~538, ActiveX 콘트롤에서 구현하는 TranslateAccelerator를 설명함.
    - pp.572~573, 컨테이너에서 처리하는 PreTranslateAccelerator를 설명. 
[3] Jeff Prosise, "Programming Windows with MFC (Second Edition)", 1999.
     - pp.190~193 : MFC CFrameWnd 클래스에서 Keyboard Accelerator 다루는 법 설명함.
     - pp.446~449 : 다이얼로그에서 Keyboard Accelerator 다루는 법 설명함.
[4] 관련 프로젝트 1 : D:\Z\SkdTestA\
     - 여러가지 상황에서 (Pre)TranslateAccelerator를 구현한 각종 콘트롤을 만든 프로젝트.
[5] 관련 프로젝트 2 : D:\KDSONG\Study\Visual C++\Window\ControlViewer\
     - 콘트롤의 Accelerator를 지원하기 위해서는 프레임 윈도우에서 이를 지원하도록
        작업해야 한다. 이 프로젝트에서 그 기능을 알 수 있다.
[6] MSDN | "PRB: Focus and Tab Issues with ATL Subclassed Edit Control"
    - ms-help://MS.MSDNQTR.2002JAN.1033/kbvc/Source/visualc/q179696.htm
    -
[*] Document path : Outlook/Visual C++/Visual C++/ATL COM ActiveX folder