ActiveX

고급 액티브X 컨트롤에 도전하자

디버그정 2008. 9. 14. 20:43

  지난 연재에서는 컨트롤의 내부를 살펴봤다. 이번호에서는 보다 좋은 컨트롤을 만들기 위한 외부적인 기능들을 몇 가지 소개하고 3회에 걸친 연재를마치려고 한다. 컨트롤이 IE 안에서 다양한 기능을 수행하기 위해 기본적으로 제공해야 할 기능이 있다. 이번호에서는 메모리 DC를 사용한 부드러운 화면 처리, 툴팁의 제공, 쓰레드의 활용 등 대표적인 기능들을 하나씩 살펴보겠다.

  웹을 기반으로 하는 프로젝트에서 중요 기능을 액티브X 컨트롤로 개발하기 위해서는 몇 가지 선행 지식이 있어야 한다. 우리는 지난 1회 연재를 통해 전체 개발 프로세스를 알아왔다. 컨트롤을 어떻게 만들고 어떻게 분배하는지를 볼 수 있었을 것이다. 2회 연재에서는 좀더 이론적인 부분으로 들어가 IE의 내부적인 구조와 이 구조에 따른 컨트롤의 특성에  대해 살펴봤다. 이번호에서는 컨트롤을 제작함에 있어 기본적으로 사용되는 기능적인 요소 몇 가지를 설명함으로써 여러분이 컨트롤을 제작하는 데 작은 도움이 됐으면 한다.
  컨트롤을 제작하는 데 필요한 요소 기술들은 아주 다양한다. 또 프로젝트의 성격에 따라 일반적이지않은 기술을 적용해야 하는 경우도 많다. 여기서는 그 중 일반에 해당하는 기술을 알려주고자 한다. 지면이 한정된 관계로 자세한 설명은 힘들기 때문에 어쩌면 기술의 열거 형식을 취하게 될지도 모른다. 이를 보충키 위해 이달의 디스켓을 통해 자세한 샘플 소스를 제공한다. 간단한 설명과 함계 제공되는 샘플 코드를 잘 이용해 줄 것을 먼저 당부한다.

소켓을 이용한 서버와의 통신
  컨트롤을 개발하면서 대부분 서버와의 통신을 사용한다. 여러가지 통신방법이 있겠지만 그 중 소켓통신이 가장 많이 이용된다. 물론 컨트롤도 일반 애플리케이션처럼 소켓통신을 사용할 수 있다. 소켓 통신에 대해서는 이미 본지에서 여러번 소개했으므로 자세한 설명은 생략한다. 다만 가끔 필자에게 소켓통신 문제로 질문하는 개발자들이 인스턴스 초기화 과정에서 소켓 초기화를 하는 것을 잊고선 통신이 되지 않는다고 문의하기에 이를 주의할 것을 당부한다. 다음은 컨트롤의 InitInstance() 함수에서 소켓을 초기화하는 예이다(자세한 소스는 이달의 디스켓을 Sock 폴더를 참조하기를 바란다.)

BOOL CSockApp::InitInstance()
{
  BOOL bInit = COleControlModule::InitInstance();
  if(bInit)
  {
   // TODO: Add your own module initialization code here.
   if(!AfxSocketInit())
   {
      AfxMessageBox("소켓을 초기화할 수 없습니다.");
      return FALSE;
   }
  }
  return bInit;
}


  다음 예는 마우스 클릭을 할 경우 특정 웹 서버에 접속해 시작페이지를 받아오는 샘플이다. 기본적인 소켓 통신에 대한 코드를 넣었는데, 여기서는 동기식 소켓통신을 사용했다. 필요에 따라 비동기 소켓을 사용할 수도 있다.
void CSockCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
   // Sock.cpp의 소켓 초기화를 꼭 해야 함
  CSocket s;
  if(s.Create() && s,connect("www.targetdomain.com",80))
  {
    // 첫 페이지 요청
    CString strSentMsg = "Get / HTTP1.0\r\n\r\n";
    s.Send(strSentMsg, strSentMsg.GetLength());
   
    // 첫 페이지 받기
    char buff[256];
    int nReceivedLen = 0;
    CString strWebPageContent;
    while((nReceivedlen = s.Receive(buff, 256)) > 0)
    {
      strWebPageContent += CString(buff, nReceivedLen);
    }
    // 받은 페이지를 출력
    MessageBox(strWebPageContent);
    s.Close();
  }
  else
  {
    // 접속 실패
    MessageBox("해당 서버에 접속할 수 없습니다.");
  }
  COlecontrol::OnLButtonUp(nFlags, point);
}

  화면1은 예제를 실행한 화면이다. 대상 웹 서버에서 받아온 페이지를 보여주고 있다.

사용자 삽입 이미지

메모리 DC의 활용
  컨트롤은 보통 화면을 가진다. 따라서 화려한 외양을 가졌거나 여러 이미지를 사용한 컨트롤을 종종 볼 수 있다. 어떤 경우에는 역동적으로 움직이는 컨트롤도 있다. 이러한 화면을 구현할 때는 깜빡임이 없고 부드러운 화면을 보여줘야 한다. 이때 자연스러운 화면처리를 위해 메모리 DC의 사용은 필수다. 화면 처리에 있어 OnDraw 함수에 들어오는 DC에 바로 출력을 하는 것은 좋은 방법이 아니다. 대신 더블버퍼링 방식을 통해 화면처리를 해야 한다. 즉 미리 메모리 DC를 만들어 두고 이 메모리 DC에 보여줄 화면을 모두 그린 후 메모리 DC를 화면 DC로 일괄 복사함으로써 깜빡임을 최소화하는 것이다.
  다음 두 함수는 메모리DC를 생성하고 제거하는 함수들이다. 메모리DC는 컨트롤의 OnCreate() 함수에서 생성되고 OnDestroy() 함수에서 제거될 것이다. (자세한 소스는 이달의 이스켓 Image 폴더 참조)

 //메모리 DC를 생성한다.
void CImageCtrl::CreateMemDC(CWnd *w, int nW, int nH)
{
  DestroyMemDC();
  CClientDC cdc(this);
  m_hMemDC = CreateCompatibleDC(cdc.m_hDC);
  m_nSaveDC = SaveDC(m_hMemDC);
  m_hBitmap = CreateCompatibleBitmap(cdc.m_hDC, nW, nH);
  m_OldBitmap = (HBITMAP)::SelectObject(m_hMemDC, m_hBitmap);
}

// 메모리 DC를 해제한다.
void CImageCtrl::DestroyMemDC()
{
  if(m_hMemDC)
  {
    ::SelectObject(m_hMemDC, m_OldBitmap);
    ::RestoreDC(m_hMemDC, m_nSaveDC);
    ::DeleteDC(m_hMemDC);
    ::DeleteObject(m_hBitmap);
  }
}


  실제 화면을 처리하는 OnDraw()에서는 메모리DC(m_hMemDC)에 화면처리를 한 후 StretchBlt 함수를 사용해 화면을 일괄 복사한다. 다음 예는 화면에 그리는 작업이 한 가지 밖에 없지만 그리는 작업이 많은 경우 OnDraw()에 인자로 들어오는 pdc에 직접 그리는 것보다 훨씬 부드러운 화면을 볼 수 있다.

void CImageCtrl::OnDraw(CDC *pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
  // TODO: Replace the following code
  // with your own drawing code.
  if(m_hMemDC)
  {
    // 메인 스킨 표시
    m_bmeImg[m_nCurrImage].Paint(m_hMemDC,
      CRect(rcBounds.left, rcBounds.top,
        rcBounds.left+ m_bmeImg[m_nCurrImage].Width(),
        rcBounds.top + m_bmeImg[m_nCurrImage].Height()),
      CRect(0,0,m_bmeImg[m_nCurrImage].Widht(),
        m_bmeImg[m_nCurrImage].Height()));
    ::StretchBlt(pdc->m_hDC, 0, 0,
        m_bemImg[m_mCurrImage].Width(),
        m_bemImg[m_mCurrImage].Height(), m_hMemDC, 0, 0,
        m_bemImg[m_mCurrImage].Width(),
        m_bemImg[m_mCurrImage].Height(), SRCCOPY);
  }
}

이미지 출력
  보기 좋은 컨트롤을 만들기 위해서는 기본적으로 이미지 출력에 신경써야 한다. 경우에 따라 소켓을 이용해 서버에서 이미지를 받아보여줄 경우도 있다. 물론 메모리 DC를 이용해 화면처리를 한다면 더욱 자연스러운 화면을 보여줄 수 잇을 것이다.
  이미지 포맷은 여러 방법을 사용할 수 있지만, 일단 비트맵 이미지를 사용해 리소스 파일에 포함하는 경우와 이미지를 컨트롤 외부에 두는 경우로 양분할 수 있다. 비트맵 이미지를 사용하는 경우 가능한 이미지의 크기를 줄이고 색 수를 256색으로 하면 더 작아질 수 있다. 리소스의 비트맵 이미지는 컨트롤의 사이즈와 직접적으로 연관돼 있으므로 더욱 주의가 필요하다. 하지만 경우에 따라 24비트 BMP의 사용이 불가피한 경우도 있다.
 
  여기서는 간단히 비트맵 이미지를 출력해 주는 클래스만을 소개한다. 다른 포맷의 이미지 출력에 대한 소스는 codeguru 같은 곳에서 쉽게 찾을 수 있다. 리스트1은 비트맵을 출력해 주는 클래스이다. 이 클래스에 대한 자세한 사용법과 소스는 이달의 디스켓의 Image 폴더를 참조하자.

  사용법은 간단하다. 먼저 리소스에서 이미지를 로드하고 화면에 출력시키면 된다.

CBitmapExt m_bmeImg;
// 이미지 로드
m_bmeImg.LoadBimap(IDB_BITMAP1);
// 이미지 출력(메모리 DC 핸들, 출력될 크기, 출력할 크기)
m_bmeImg.Paint(m_hMemDC, CRect(rcbounds.left, rcBounds.top,
          rcBounds.left+m_bmeImg.Width(), rcBounds.top+m_bmeImg.Height()),
          CRect(0,0, m_bmeImg.Width(), m_bmeImg.Height()));

  화면2는 CBitmpaExt 클래스를 이용해 컨트롤에서 이미지를 출력한 결과이다.

사용자 삽입 이미지

참고) IE의 Stop 버튼 지원하기
  IE 안에서 동작하는 컨트롤은 IE에서 발생되느 이벤트를 알아야 하는 경우가 있다. 가장 대표적인 경우가 IE의 STOP 버튼을 눌렀을 때 컨트롤의 동작이 동시에 멈추게 하는 것이다. 이것이 가능하려면 컨트롤은 IOleCommandTarget 인터페이스를 구현해야 한다. 하지만 COleControl 클래스는 이미 이 인터페이스를 기본적으로 구현해주지 않기 때문에 직접 구현해야 한다. 자세한 소스는 이달의 디스켓 StopTest 폴더를 참조한다.
  먼저 다음과 같이 헤더에 인터페이스를 선언한다.
class CStopTestCtrl : public COleControl
{
...
...
...
// Interface Maps
protected:
 // Add the following to support the IOleCommandTarget interface.
 // NOTE:  Nested class name is called CmdTargetObj
 DECLARE_INTERFACE_MAP()

 BEGIN_INTERFACE_PART(CmdTargetObj, IOleCommandTarget)
   STDMETHOD(QueryStatus)(const GUID*, ULONG, OLECMD[], OLECMDTEXT*);
   STDMETHOD(Exec)(const GUID*, DWORD, DWORD, VARIANTARG*,
   VARIANTARG*);
 END_INTERFACE_PART(CmdTargetObj)
};

  그리고 나서 구현부분에 다음 구현코드를 추가하고 Exec() 함수에 원하는 코드를 추가하면 된다. 다음 예는 IE의 정지버튼을 누르면 메시지를 출력하고 사용하던 타이머를 정지하는 코드이다. IE의 명령어 중 OLECMDID_STOP 이외에도 OLECMDID_REFRESH 등 사용할 만한 몇 개의 OLEDMDID들이 있다.

BEGIN_INTERFACE_MAP(CStopTestCtrl, COleControl)
 INTERFACE_PART(CStopTestCtrl, IID_IOleCommandTarget, CmdTargetObj)
END_INTERFACE_MAP()

ULONG FAR EXPORT CStopTestCtrl::XCmdTargetObj::AddRef()
{
 METHOD_PROLOGUE(CStopTestCtrl, CmdTargetObj)
 return pThis->ExternalAddRef();
}

ULONG FAR EXPORT CStopTestCtrl::XCmdTargetObj::Release()
{
 METHOD_PROLOGUE(CStopTestCtrl, CmdTargetObj)
 return pThis->ExternalRelease();
}

HRESULT FAR EXPORT CStopTestCtrl::XCmdTargetObj::QueryInterface(REFIID iid, void FAR* FAR* ppvObj)
{
 METHOD_PROLOGUE(CStopTestCtrl, CmdTargetObj)
 return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP CStopTestCtrl::XCmdTargetObj::QueryStatus(const GUID* pguidCmdGroup, ULONG cCmds, OLECMD rgCmds[],OLECMDTEXT* pcmdtext)
{
 METHOD_PROLOGUE(CStopTestCtrl, CmdTargetObj)
 //... add YOUR own code here.

 return S_OK;
}

STDMETHODIMP CStopTestCtrl::XCmdTargetObj::Exec(const GUID* pguidCmdGroup, DWORD nCmdID, DWORD nCmdExecOpt,VARIANTARG* pvarargIn, VARIANTARG* pvarargOut)
{
 METHOD_PROLOGUE(CStopTestCtrl, CmdTargetObj)
 if (nCmdID == OLECMDID_STOP)
 {
  // ... STOP button is clicked, add YOUR own code here.
  // We just display a message box.
  //::MessageBox(NULL, "STOP","CStopTestCtrl", MB_OK);

  //타이머를 정지시킨다.
  if(IsWindow(pThis->m_hWnd)) pThis->KillTimer(0);
 }

 return S_OK;
}

툴팁의 사용
  현재 웹에서 볼 수 있는 컨트롤의 경우 툴팁을 제공하는 경우가 많지 않다. 하지만 보다 쉬운 사용자 인터페이스를 제공하기 위해 툴팁은 필수다. 일반 MFC 애플리케이션에서 툴팁을 제공하는 유형은 다음과 같이 크게 두 가지로 구분한다.
 1) 액티브X 컨트롤이 소유한 자식컨트롤에 대한 툴팁 제공
 2) 액티브X 컨트롤의 특정 영역에 툴팁 제공

  화면3과 화면4는 1과 2의 예이다. 두 경우에 대한 처리방법은 애플리케이션과 컨트롤 모두에서 동일하다. 다만 컨트롤의 특성으로 인해 추가 작업이 필요하다. 먼저 이들 방법에 대해 알아보고 컨트롤에 추가할 사항을 살펴보자.

사용자 삽입 이미지

사용자 삽입 이미지


액티브X 컨트롤이 소유한 자식 컨트롤에 대한 툴팁 제공

  먼저 자식 버튼을 생성하자. 그리고 툴팁을 사용하도록 지정한다. 자세한 소스는 이달의 디스켓 ToolTipTest2 폴더 참조.

int CToolTipTest2Ctrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  if(COleControl::OnCreate(lpCreateStruct) == -1) return -1;

  // TODP: Add your specialized creation code here
  m_btnTest1.Create("테스트1", BS_PUSHBUTTON|WS_CHILD|WS_TABSTOP,
             CRect(10,10, 100, 30), this, IDC_TEST1);
  m_btnTest2.Create("테스트2", BS_PUSHBUTTON|WS_CHILD|WS_TABSTOP,
             CRect(10,35, 100, 55), this, IDC_TEST2);
  //툴팁을 사용한다.
  EnableToolTips(TRUE);
  return 0;
}

  메시지맵에 TTN_NEEDTEXT 메시지 처리 매크로를 추가하고 해당 핸들러 함수를 구현한다. 이 핸들러 함수는 각 컨트롤에 대해 어떤 툴팁이 나와야 하는지를 정하는 부분이다.

  BEGIN_MESSAGE_MAP(CToolTipTest2Ctrl, COleControl)
    ...
    ...
    ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, OnToolTipNotify)
  END_MESSAGE_MAP()

  // 각 컨트롤에 대한 툴팁 문자 지정
  BOOL CToolTipTest2Ctrl::OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult)
  {
    TOOLTIPTEXT*  pText = (TOOLTIPTEXT)pNMHDR;
    int control_id = ::GetDlgCtrlID((HWND)pNMHDR->idFrom);
    switch(control_id)
    {
      case IDC_TEST1:   // 리소스에 저장된 문자열을연결한다.
      case IDC_TEST2:
          pText->lpszText= MAKEINTRESOURE(control_id);
          pText->hinst = AfxGetInstanceHandle();
          return TRUE;
          break;
    }
  return FALSE;
  }

액티브X 컨트롤들의 특정영역에 대한 툴팁 제공
  우리는 컨트롤의 특정 영역에 어떤 기능을 부여하고 여기에 마우스가 왔을 때 툴팁을 제공해 줄 필요가 있다. 이런 경우 툴팁을 지정하는 방법을 살펴보겠다. 이러한 툴팁을 위해 MFC는 CToolTipCtrl이라는 컨트롤을 제공한다. 그럼 이 툴팁 컨트롤을 어떻게 사용하는지 알아보자.(자세한 소스는 이달의 디스켓 ToolTipTest 폴더 참조)
  먼저 리스트2와 같이 컨트롤 클래스의 헤더에 CToolTipCtrl변수와 RelayEvent라는 함수를 추가하자. RelayEvent 함수는 컨트롤에 발생된 각종 마우스 이벤트를 툴팁 컨트롤에게 전달하는 역할을 수행한다. ToolTipTextCtl.cpp 리스트3에서는 OnCreate() 함수에서 툴팁 컨트롤을 생성하고 툴팁 컨트롤에게 툴팁 영역과 해당하는 문자열을 등록하고 OnLButtonDown, OnLButtonUp, OnMouseMove 함수에서 RelayEvent 함수를 호출한다.

<리스트2 내용>
class CToolTipTestCtrl : public COleControl
{
  CToolTipCtrl m_ttToolTip;
  void RelayEvent(UINT message, WPARAM wParam, LPARAM lParam);
....
};


<리스트3 내용>
/////////////////////////////////////////////////////////////////////////////
// CToolTipTestCtrl message handlers
int CToolTipTestCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (COleControl::OnCreate(lpCreateStruct) == -1)
  return -1;
 
 // TODO: Add your specialized creation code here
 if( !m_bmeSkin.LoadBitmap( IDB_MAINSKIN ) ) return -1;

 //툴팁 지정
    m_ttToolTip.Create (this, TTS_ALWAYSTIP);
 m_ttToolTip.AddTool(this,"여기는 좌상단의 것",CRect(7,3+20,39,35+20),1);
 m_ttToolTip.AddTool(this,"여기는 우상단의 것",CRect(78,0+20,144,65+20),2);
 m_ttToolTip.AddTool(this,"여기는 중간의 것",CRect(41,42+20,73,74+20),3);
 m_ttToolTip.AddTool(this,"여기는 우하단의 것",CRect(112,67+20,144,99+20),4);
 m_ttToolTip.AddTool(this,"여기는 좌하단의 것",CRect(5,85+20,91,119+20),5);
 m_ttToolTip.Activate (true);

 // Because ActiveX control is an inproc server, it does not have a
     // message pump. So, messages to child windows created by the
     // ActiveX control are not going to be received by the control.
     // Thus, we set up a message hook to call PreTranslateMessage().
     // This results in the call to FilterToolTipMessage(), which
     // activates tooltips.
     hHook = ::SetWindowsHookEx(
        WH_GETMESSAGE,
        GetMessageProc,
        AfxGetInstanceHandle(),
        GetCurrentThreadId());
     ASSERT (hHook);

 return 0;
}

void CToolTipTestCtrl::OnDestroy()
{
 COleControl::OnDestroy();
 
 // TODO: Add your message handler code here
 VERIFY (::UnhookWindowsHookEx (hHook));
}

void CToolTipTestCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
 // TODO: Add your message handler code here and/or call default
 RelayEvent(WM_LBUTTONDOWN, (WPARAM)nFlags,
                    MAKELPARAM(LOWORD(point.x), LOWORD(point.y)));

 COleControl::OnLButtonDown(nFlags, point);
}

void CToolTipTestCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
 // TODO: Add your message handler code here and/or call default
 RelayEvent(WM_LBUTTONUP, (WPARAM)nFlags,
                    MAKELPARAM(LOWORD(point.x), LOWORD(point.y)));

 COleControl::OnLButtonUp(nFlags, point);
}

void CToolTipTestCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
 // TODO: Add your message handler code here and/or call default
 RelayEvent(WM_MOUSEMOVE, (WPARAM)nFlags,
                    MAKELPARAM(LOWORD(point.x), LOWORD(point.y)));

 COleControl::OnMouseMove(nFlags, point);
}

void CToolTipTestCtrl::RelayEvent(UINT message, WPARAM wParam, LPARAM lParam)
{
 if (NULL != m_ttToolTip.m_hWnd)
 {
  MSG msg;

  msg.hwnd= m_hWnd;
  msg.message= message;
  msg.wParam= wParam;
  msg.lParam= lParam;
  msg.time= 0;
  msg.pt.x= LOWORD (lParam);
  msg.pt.y= HIWORD (lParam);

  m_ttToolTip.RelayEvent(&msg);
 }
}


메시지 후킹
  앞서 이야기했던 것과 같이 컨트롤과 애플리케이션의 툴팁 구현의 차이는 컨트롤은 훅킹을 사용해야 한다는 점이다. IE 안에서 동작하는 컨트롤은 컨트롤의 상태에 따라 마우스 이벤트에 대해 PreTranslateMessage() 함수가 제대로 호출되지않는다. 따라서 컨트롤이 툴팁을 보이기 위해서는 GetMessage 함수의 호출을 가로채 직접 PreTranslateMessage() 함수를 호출해야 한다.
  리스트4는 후킹함수를 구현한 예이다. OnCreate() 함수에서 SetWindowsHookEx() 함수를 호출해 등록하면 된다.

<리스트4 내용>
HHOOK hHook = NULL;

//툴팁을 띄우기 위해 WH_GETMESSAGE를 가로챈다.
//콘트롤은 PreTranslateMessage를 사용할 수 없기 때문이다.
// Hook procedure for WH_GETMESSAGE hook type.
LRESULT CALLBACK GetMessageProc(int nCode, WPARAM wParam, LPARAM lParam)
{
 // Switch the module state for the correct handle to be used.
 AFX_MANAGE_STATE(AfxGetStaticModuleState( ));

 // If this is a keystrokes message, translate it in controls'
 // PreTranslateMessage().
 LPMSG lpMsg = (LPMSG) lParam;

 if( (nCode >= 0) &&
   PM_REMOVE == wParam &&
   AfxGetApp()->PreTranslateMessage(lpMsg))
 {
    lpMsg->message = WM_NULL;
    lpMsg->lParam = 0L;
    lpMsg->wParam = 0;
 }


 // Passes the hook information to the next hook procedure in
 // the current hook chain.
 return ::CallNextHookEx(hHook, nCode, wParam, lParam);
}

/////////////////////////////////////////////////////////////////////////////
// CToolTipTestCtrl message handlers

int CToolTipTestCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (COleControl::OnCreate(lpCreateStruct) == -1)
  return -1;
 
 // TODO: Add your specialized creation code here
 if( !m_bmeSkin.LoadBitmap( IDB_MAINSKIN ) ) return -1;

 //툴팁 지정
    m_ttToolTip.Create (this, TTS_ALWAYSTIP);
 m_ttToolTip.AddTool(this,"여기는 좌상단의 것",CRect(7,3+20,39,35+20),1);
 m_ttToolTip.AddTool(this,"여기는 우상단의 것",CRect(78,0+20,144,65+20),2);
 m_ttToolTip.AddTool(this,"여기는 중간의 것",CRect(41,42+20,73,74+20),3);
 m_ttToolTip.AddTool(this,"여기는 우하단의 것",CRect(112,67+20,144,99+20),4);
 m_ttToolTip.AddTool(this,"여기는 좌하단의 것",CRect(5,85+20,91,119+20),5);
 m_ttToolTip.Activate (true);

 // Because ActiveX control is an inproc server, it does not have a
     // message pump. So, messages to child windows created by the
     // ActiveX control are not going to be received by the control.
     // Thus, we set up a message hook to call PreTranslateMessage().
     // This results in the call to FilterToolTipMessage(), which
     // activates tooltips.
     hHook = ::SetWindowsHookEx(
        WH_GETMESSAGE,
        GetMessageProc,
        AfxGetInstanceHandle(),
        GetCurrentThreadId());
     ASSERT (hHook);

 return 0;
}


비동기 데이터 송수신
  웹 환경에서 동작하는 컨트롤의 특성상 컨트롤이 사용하는 데이터가 서버상에 존재하는 경우가 많다. 이러한 데이터를 받는 과정에서 비동기 데이터 송수신은 필수사항이다. 컨트롤이 초기에 사용할 데이터를 모두 받고 컨트롤이 초기화 과정을 수행한다면 아마 사용자는 오랫동안 컨트롤의 화면을 볼 수 없을 것이다. 우리는 사용할 데이터의 크기가 큰 경우, 이를 모두 받고 화면에 표시할 수는 없다. 오히려 컨트롤이 먼저 떠서 데이터를 수신하는 과정을 진행바 형태로 보여줘야 할 필요가 있다.
  데이터 송수신을 비동기로 처리하는 방법에는 여러가지가 있는데 크게 보면 비동기 함수를 사용하는 것과 쓰레드를 사용하는 두 가지 방식으로 나뉜다.


비동기 함수의 사용
  io 함수는 대부분 비동기 호출을 지원하므로 이러한 함수를 사용해 서버의 데이터를 받아올 수 있다. 이런 경우 진행상태를 화면에 나타낼 수 있다. 도 다른 방법은 액태브X 컨트롤의 비동기 속성을 이용하는 방법이다. 이 방식으로는 많은 수의 데이터를 송수신할 수는 없겠지만 초기 데이터를 받는 데는 사용할 수 있다.
  비동기 속성을 사용하는 것은 지난 연재에서 연급한 바와 같이 비동기 속성을 지정하는 방식을 이용해 처리한다. MSDN의 Internet First Steops: ActiveX Controls 의 문서를 참조하면 이와 관련된 자세한 설명을 볼 수 있다.

참고) iostream 관련 함수의 사용
MFC로 컨트롤을 제작하다보면 여러 외부 소스를 같이 사용하게 된다. 이들 소스 중에는 iostream 관련 함수를 사용하는 경우가 있다. 즉 << 연산자나 >> 연산자를 지원해 iostream을 사용하게 되는데, 이럴 경우 MFC의 기본 DLL 외에도 msvcirt.dll을 사용하게 된다. 따라서 이 Dll의 배포에도 주의해야 한다. 가능하면 이런 함수느 제거하고 사용하는 것이 컨트롤 배포시 크기를 줄이는 데 도움이 된다.

스레드의 사용
  스레드의 사용은 Win32 환경에서는 아주 자연스러운 방법이다. 스레드 사용방법에 대해서는 이미 많은 연재가 있었으므로 여기서는 자세한 설명보다는 간단한 사용법만 설명하겠다. 스레드는 데이터 송수신 이외에도 많은 시간으 요하는 처리를 하는 경우 사용하면 좋다. 물론 동기화를 잘 시켜야 한다는 부담이 있겠지만 잘 사용하면 좋은 컨트롤을 만들 수 있다.
  또한 WinINet.dll에는 많은 인터넷 관련 함수들을 제공해주고 있는데 구중 URLDownloadToCacheFile() 함수나 URLDownloadToFile() 함수들은 비록 블로킹 함수지만 서버로부터 데이터를 받는 기능을 제공하는 유용한 함수들이다. 이들 함수를 쓰레드와 같이 사용한다면 더 좋은 효과를 얻을 수 있다. 자세한 소스는 이달의 디스켓 ThreadTest 폴더 참조.

HyperLink 사용
  액티브X 컨트롤은 사용자와의 상호 작용에 의해 다른 페이지로 이동해야 할 필요가 있게 된다. 과거 IE 3.x 시대에서는 단 한 가지 방법(WinInet.dll 함수사용) 밖에는 없었지만, 현재는 다음과 같이 여러 방법이 있다. 자세한 소스는 이달의 디스켓의 HyperLink Test 폴더 참조.

1) WinInet.dll의 함수 사용 : 가장 오래된 방법
#include <urlmon.h>
#include <ATLCONV.h>
void CHyperLinkTestCtrl::OnLink()
{
  USES_CONVERSION:
  LPCWSTR target = T2COLE("msdn.microsoft.com");
  HlinkNavigateString(GetInterfaceHook(m_piidPrimary), target);
}

2) 이벤트를 사용하는 방법 : 필요한 경우 사용한다.
<script for=Start event=ChangeMyURL() language="javaScript">
<!--
    document.location.href="http://www.aaa.co.kr";
//-->
</script>

3) IHTMLDocument2를 사용한 방법
#include <mshtml.h>
void CHyperLInkTestCrtl::OnLink()
{
  IOlecontainer* pContainer = NULL;
  // 컨테이너의인터페이스를 얻고
  m_pClientsite->GetContainer(&pContainer);
  if(pContainer != NULL)
  {
    IHTMLDocuments* pDoc=NULL;
    // IID_IHTMLDocuments 인터페이스를 얻고
    HRESULT hr = pContainer->QueryInterface(
         IID_IHTMLDocument2, (void**)&pDoc);
    if(SUCCEEDED(hr))
    {
      CString strUrl("http://www.javasoft.com");
      pDoc->put_URL(strUrl.AllocSusString());
      pDoc->Release();
    }
  }
}


 

참고) 두번째 스레드에서의 이벤트 발생
  IE 4.x 에서 스레드 아키텍쳐가 변경되면서 컨트롤이 생성한 스레드의 컨트롤 사이의 함수 콜에 몇가지 제약이 따르게 됐다. 특히 이벤트 발생 같은 COM 관련 함수들은 제대로 동작하지 않는다. 이들은 별도의 마샬링을 해줘야 가능하게 됐다. 그래서 MS에서는 생성된 두번째 스레드에서는 컨트롤의 COM 관련 함수를 호출하지 말고 사용자 정의 윈도우 메시지를 사용해 컨트롤에 알리고 컨트롤이 직접 호출하도록 권하고 있다. 컨트롤을 만들면서 스레드를 자주 사용하다 보면 이런 부분의 에러를 접하는 경우가 간혹 있으므로 참조하기 바란다.


<리스트5 내용>
class CThreadTestCtrl : public COleControl
{
 DECLARE_DYNCREATE(CThreadTestCtrl)

 //데이터 수신을 처리하는 쓰레드에 관련한 변수와 함수들
 int   m_nPercent;
 CWinThread* pWaitThread;
 static UINT RecvDataThreadProc( LPVOID pParam );
 ...
 ...
};

/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl::CThreadTestCtrl - Constructor

CThreadTestCtrl::CThreadTestCtrl()
{
 InitializeIIDs(&IID_DThreadTest, &IID_DThreadTestEvents);

 // TODO: Initialize your control's instance data here.
 m_nPercent   = 0;
 pWaitThread         = NULL;
}


/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl::~CThreadTestCtrl - Destructor

CThreadTestCtrl::~CThreadTestCtrl()
{
 // TODO: Cleanup your control's instance data here.
 //아직 쓰레드가 살아 있으면 종료시킨다.
 if(pWaitThread)
 {
  DWORD dwExitCode;
  if (::GetExitCodeThread(pWaitThread->m_hThread, &dwExitCode) &&
   dwExitCode == STILL_ACTIVE)
  {
   TerminateThread(pWaitThread->m_hThread , dwExitCode);
  }

  delete pWaitThread;
 }
}


/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl::OnDraw - Drawing function

void CThreadTestCtrl::OnDraw(
   CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
 // TODO: Replace the following code with your own drawing code.
 pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
 CString msg;
 msg.Format("다운로드 %d %%", m_nPercent);
 pdc->TextOut(10,10,msg);
}


/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl::DoPropExchange - Persistence support

void CThreadTestCtrl::DoPropExchange(CPropExchange* pPX)
{
 ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
 COleControl::DoPropExchange(pPX);

 // TODO: Call PX_ functions for each persistent custom property.

}


/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl::OnResetState - Reset control to default state

void CThreadTestCtrl::OnResetState()
{
 COleControl::OnResetState();  // Resets defaults found in DoPropExchange

 // TODO: Reset any other control state here.
}

//데이터파일을 로드하는 쓰레드의 처리함수
UINT CThreadTestCtrl::RecvDataThreadProc( LPVOID pParam )
{
 CThreadTestCtrl* pParent = (CThreadTestCtrl*)pParam;

 for(int i=0; i<100; i++)
 {
  //통신처리를 수행한다.
  //URLDownloadToCacheFile() 함수 같은 것을 사용하면 좋을 것이다.

  pParent->m_nPercent++; //이 부분은 동기화 객체를 사용해야 한다.
  pParent->Invalidate(); //화면 갱신
  Sleep(1000); //테스트를 위해 시간을 지연시킨다.
 }

 return 0;
}

/////////////////////////////////////////////////////////////////////////////
// CThreadTestCtrl message handlers

int CThreadTestCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (COleControl::OnCreate(lpCreateStruct) == -1)
  return -1;
 
 // TODO: Add your specialized creation code here
 //서버로부터 데이터를 백그라운드로 받는 쓰레드 가동
 pWaitThread = AfxBeginThread( RecvDataThreadProc, this,
                                             THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);
 pWaitThread->m_bAutoDelete = FALSE; //CWinThread 객체의 자동 제거를 해제한다.
 pWaitThread->ResumeThread();

 return 0;
}




엔터키를 지원하는 에디트 박스
  일반 애플리케이션의 경우도 마찬가지지만 입력칸에서 사용자가 엔터키를 친 경우, 여러가지 처리를 해야하는 경우가 있다. 하지만 일반적으로 에디트 컨트롤에서 엔터키가 발생했는지 메인 컨트롤은 알 수 없다. 그래서 사용자 정의 윈도우 메시지를 사용해 엔터가 눌려진 경우 메시지를 보내주는 에디트 클래스가 필요하게 되는데 그것이 바로 CEnterableEdit 클래스이다. 필자도 간혹 사용자에게 보다 편린한 화면을 제공하기 위해 사용하는 에디트 클래스이다. CEnterableEdit 클래스의 정의와 구현은 다음과 같다. 자세한 소스는 이달의 디스켓 EditsTest 폴더 참조.

#include "ImeEdit.h"
#define WM_ENTERABLEEDIT_ENTERKEYDOWN WM_USER+57
// 57은 임의의 숫자

class CEnterableEdit : public CImeEdit
{
  afx_msg void OnKeyDown(
    UINT nChar, UINT nRepCnt, UINT nFlags);
};

void CEnterableEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
  if(nChar == VK_RETURN)
  {
    int ctrlId = GetWindowLong(m_hWnd, GWL_ID);
    if(ctrlId != 0)
    {
      CWnd* pParentWnd = GetParent();
      ASSERT(pParentWnd != NULL);
      pParentWnd->PostMessage(
          WM_ENTERABLEEDIT_ENTERKEYDOWN, ctrlId, 0L);
    }
  }
  CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}


한글, 영문을 지원하는 에디트 컨트롤

  IME 제어 기술에 대해서는 여러 차례 본지에서 소개했다. 입력 컨트롤에서 한영모드를 조정하고자 하는 경우가 종종 있다. 물론 아는 사용자에게 조금이라도 편리한 화면(가령 입력칸에 포커스가 가면 알아서 한글모드로, 때론 영문모드로 자동 지정되는 기능)을 제공하고자 하는 아주 기특한 노력일 것이다. 필자도 한영처리를 지원하는 에디트컨트롤 클래스를 만들어 사용하곤 한다. CImeEdit는 한글로 지정한 경우 사용자가 영문모드로 변경하는 것을 막는 기능도 제공하기 때문에 경우에 따라 아주 유용하다. 이달의 디스켓 EditsTest 폴더 참조. 클래스의 정의는 다음과 같다. SetHangeulMode()라는 함수를 이용해 입력 컨트롤에 한영모드를 지정해 준다.

// CImeEdit
// 한영모드에 대한 제어가 가능한 에디트컨트롤 클래스
// 기본적으로 한글 모드이다.
class CImeEdit : public CEdit
{
public:
  // SetHangeulMode
  // 포커스를 받을 때 입력모드
  // bHangeulMode : 한글모드 사용여부 TRUE: 사용, FALSE: 사용안함
  // bModFix : 모드변경 가능여부 TRUE: 불가능, FALSE: 가능
  void SetHangeulMode(BOOL bHangeulMode = TRUE, BOOL bModeFix=FALSE);

  // SetRememberFlag
  // 자신의 입력모드를 포커스를 잃을 때 보관할 것인지 여부 결정
  // bRememberFlag: 보관 여부 TRUE: 보과, FALSE:  보관하지 않음
  void SetRememberFlag(BOOL bRemeberFlag = TRUE);
};

  사용법은 비교적 간단하다. 컨트롤을 생성하고 SetHangeulMode() 함수를 호출하면 된다.

  CImeEdit m_editImel;
  m_editImel.Create(ES_LEFT | WS_CHILE | WS_VISIBLE | WS_TABSTOP | WS_BORDER, CRect(130, 22, 250, 42), this, IDC_INE1);
  m_editImel.SetHangeulMode(TRUE, TRUE);


연재를 마치며
  이번 연재에서는 액티브X 컨트롤의 다양한 기능적 요소에 대해 다뤄봤다. 다루고 싶었으나 지면 관계상 미처 다루지 못한 내용도 많다. 대표적인 것이 컨트롤 제작시 많이 사용하게 되는 리스트 컨트롤이나 트리 컨트롤을 컨트롤안에 직접 생성하고 여기에 탐색기처럼 드래그앤드랍을 지원하는 것이라든지 컨트롤 안에 웹 브라우저 컨트롤을 띄운다든지 하는 것들이다.
  필자는 6년에 걸쳐 컨트롤을 개발하면서 그동안 많은 컨트롤을 만들었다. 이번 연재를 통해 필자도 나름대로 컨트롤을 개발하고자 하는 이들에게 전해줄 문서를 남긴 것 같아 뿌듯하다. 이를 통해 앞으로도 더욱 재미있고 유용한 컨트롤들이 많이 나오길 바라며 이 연재가 여러분에게 조그이나 도움되길 바라는 마음이다. 그동안 연재를 애독해준 독자들에게 감사의 말을 전하고 싶다.