ActiveX

IE에서의 액티브X 컨트롤

디버그정 2008. 9. 7. 14:39

IE에서의 액티브X 컨트롤



  인터넷 익스플로러는 4.0 버전에서 비로소 모듈화가 이뤄져, 각 모듈이 재사용 가능한 구조를 갖게 되었다. 더불어 컨트롤도 이 구조의 한 부분으로서 동작하며 다른 객체들에 접근할 수 있게 됐다. 이번 호에는 이러한 IE의 구조를 파악하고 그 안에서의 컨트롤의 특성을 이해해 보자.

 

시작하기

지난 호에서 우리는 기본적인 컨트롤의 생성과 배포에 대해 살펴봤다. 어떤 프로젝트라도 초기 기본틀의 완성이 매우 중요하지만 결코 그것이 다는 아니다. 액티브X 컨트롤을 제작하다 보면 많은 새로운 문제에 직면하게 된다. 이번 호에서는 이러한 문제들을 이해하기 위해 인터넷 익스플로러 내부를 살펴보고 그  안에서 컨트롤의 특성을 알아보겠다.

 

흔히 액티브X 컨트롤을 IE의 자식 윈도우쯤으로 생각하기 쉽다. 그러나 사실 전혀 그렇지 않다. IE 3.0이 나왔을 때만 해도 이 이야기는 설득력이 있었지만 IE 4.0에서는 컨트롤에 다양한 특성이 추가됐다. 그로 인해 필자를 비롯한 몇몇 개발자는 자신의 액티브X 컨트롤 코드를 많이 수정해야만 했다.

 

이에 대한 마이크로소프트의 문서는 MSDN "Building ActiveX Controls for Internet Explorer 4.0"이다. IE 4.0에서 액티브X 컨트롤들은 Apartment 쓰레드 모델을 지원해야 한다. 뿐만 아니라 액티브X 컨트롤은 InprocServer로서 동작해야 한다. 또한 IE 3.0과는 다른 Active 상태와 Inactive 상태라는 두 가지 상태를 가져 액티브X 컨트롤을 일반 차일드 윈도우와 다르게 동작하도록 만든다. 실제로 컨트롤 개발시 테스트는 대개 "Active Control Test Container"를 사용하지만, Test Container는 컨트롤을 처리하는 방법에서 IE와 다른 경우를 보게 된다.

 

IE의 버전 중 IE 3.0이 웹 브라우저 시장에서 확고한 기반을 다지는 발판을 마련했다면 IE 4.0 IE를 실제로 모듈화해 상호 연결과 확장, 재사용이 가능한 구조로 완성시켰다. 이 구조는 IE 5.5가 나온 지금까지도 유지되고 있으니 얼마나 중요한 변화였는지 알 수 있다.

 

사용자 삽입 이미지

IE 4.0의 구조

먼저 IE 4.x 버전에서 만들어진 IE의 구조를 살펴보자.

그림1에서 IEXPLORE.EXE는 익스플로러를 가동할 때 메모리에 올라오는 작은 애플리케이션이다. 이 모듈은 HTML 문서를 보이기 위해 Shodocvw.dll을 사용한다. Shdocvw.dll은 네비게이션, 즐겨찾기, 히스토리 관리 등의 기능을 수행하는데 우리가 브라우저 컨트롤이라고 말하는 것이 바로 Shdocvw.dll이다. Shdocvw.dll Mshtml.dll을 사용해 문서를 분석하고 DOM(Document Object Model) 트리를 생성한다. Mshtml.dll은 자바 가상머신. 스크립트 엔진, 액티브X 컨트롤, 플러그인 등을 처리한다.

 

우리가 웹에서 여러 개의 프레임을 사용하는 경우 각각의 프레임에 대해 Shdocvw.dll이 생성돼 처리하게 되고 이중 액티브X 컨트롤은 Mshtml.dll 안에서 처리된다. 또한 Mshtml.dll이 제공하는 IHTMLDocument2 인터페이스를 이용해 자유롭게 Mshtml.dll의 기능과 데이터를 사용할 수 있다. IHTMLDocument2 인터페이스를 이용하면 HTML 문서 안에서 자바 스크립트를 이용해 DHTML 객체를 다루듯이 컨트롤 안에서도 DHTML 객체를 다루듯이 컨트롤 안에서도 DHTML 객체들을 제어할 수 있게 된다.

 

Active 상태와 Inactive 상태

4.0으로 버전업된 IE HTML 문서가 읽혀질 때 바로 컨트롤을 활성화하지 않았다. 이는 컨트롤을 활성화하면서 생기는 많은 부하(윈도우 생성 등)를 최소화하기 위한 방안이다. IE 4에서의 이러한 변화는 사실 새로운 것은 아니었다. 이미 OC96(OLE controls 96)에서 정의했던 컨트롤의 특징을 구현한 것이다.

 

IE 4.0 이후 액티브X 컨트롤은 Active 상태와 Inactive 상태라는 두 가지 상태로 존재한다. IE 3.0에서는 모든 컨트롤은 Active 상태로 생성되는데, 사실 모든 컨트롤이 생성되자마자 윈도우 핸들을 가질 필요는 없다. 윈도우 생성에는 많은 시스템 자원이 사용되고 처리시간도 오래 걸리므로 낭비가 심하다. 그래서 IE 컨트롤이 실제로 윈도우가 필요할 때(컨트롤이 IE 클라이언트 영역 안에서 보이게 됐건, 사용자가 탭 키를 눌러 포커스가 컨트롤에 왔거나, 마우스로 컨트롤 영역을 눌렀을 경우 등)에 윈도우를 생성해 준다.

 

따라서 처음 웹페이지가 로딩됐을 때 그 페이지 안에 있는 컨트롤들은 바로 Active 상태로 들어가지 않고 사용자가 탭을 이용해 포커스를 주거나 마우스로 클릭했을 때 Active 상태로 들어간다. 물론 컨트롤 개발시 OLEMISC_ACTIVATEWHENVISIBLE 옵션을 사용해 화면에 컨트롤이 보일 때 바로 활성화할 수도 있다. 이제부터 이 두 가지 상태에 따른 컨트롤의 변화와 비주얼 C++가 지원하는 중요한 옵션을 살펴보자.

 

OnDraw 함수 사용

액티브X 컨트롤일 IE 안에서 페인트될 때 일반 MFC 응용프로그램과 비슷하게 처리된다. 하지만 컨트롤이 Active Inactive 두 가지 상태로 존재하기 때문에 상태에 따라 다르게 처리해야 하는 부분이 생긴다.

  Active 상태에 있는 컨트롤은 일반 윈도우처럼 WM_PAINT 메시지를 받고 스스로 자신의 영역을 책임지게 되지만 Inactive 상태에선 더 이상 WM_PAINT를 받지 못한다. 그래서 간혹 OnPaint에서 화면처리한 컨트롤이 자신 위의 잔상을 제대로 지우지 못해 지저분해지는 것을 볼 수 있다.

  Inactive 상태에서는 컨테이너가 대신 컨트롤의 OnDraw 함수를 호출함으로써 컨트롤이 자신의 영역을 제대로 처리할 수 있게 도와준다. 따라서 컨트롤은 화면처리를 수행할 때 반드시 OnDraw 함수안에서 해야 한다. Inactive 상태에서 컨테이너는 자신이 가진 Device Context와 컨트롤의 좌표를 OnDraw 함수를 통해 전달한다. 이러한 컨테이너의 행동은 컨트롤 개발자에게 약간의 혼동을 주기도 한다. Active 상태에서 OnDraw에 들어오는 좌표의 좌측상단은 0,0이 되지만 Inactive 상태에서는 컨테이너의 좌표를 기준으로 컨트롤의 위치 값이 들어오게 된다는 점에 주의하자. 컨트롤 개발자는 자신의 왼쪽 상단좌표가 0,0이라고 생각하면 안되면, 항상 인자로 들어온 좌표를 사용해 처리해야 한다.

 

 

참고 ---- IE4에 반영된 OC96의 주요 특징

  IE 4에서는 OC96(OLE Controls 96)에서 정의했던 컨트롤의 다음과 같은 특징들이 구현됐다.

  1. 지연된 활성화 : 컨트롤은 컨테이너에 로딩될 때 바로 활성화되지 않는다. 따라서 같은 페이지에 여러 개의 컨트롤이 포함된 경우 사용자는 보다 빨리 화면을 볼 수 있게 된다. 이때 컨트롤은 IPointInactive를 통해 최소한의 마우스 동작을 받을 수 있다.

  2. 윈도우 없는 컨트롤 : 윈도우를 갖지 않은 컨트롤을 지원하다. 따라서 컨테이너는 보다 적은 시스템 자원을 사용해 페이지를 처리할 수 있게 된다. 또한 활성화와 비활성화를 더욱 빠르게 수행한다. 컨테이너는 화면처리시 자신의 디바이스 컨텍스트를 컨트롤에 전달해 컨트롤이 화면을 처리하도록 해야 한다.

  3. 마우스 클릭 얻기 : 윈도우를 가지지 않은 컨트롤이 마우스 동작을 인식하기 위해 컨테이너는 마우스 클릭을 감시하고 만약 컨트롤의 영역에 포함된 경우 IViewObjectEx::QueryHitPoint()를 호출한다. 이때 컨트롤은 이 클릭이 자신의 영역에 클릭됐는지 결정한다.

  4. 빠른 활성화 : 새로운 인터페이스인 IQuickActivate를 통해 컨트롤을 보다 빨리 활성화시킨다. IE 4.x 이후 IE는 컨트롤이 이 인터페이스를 지원하는지 확인하고, 지원한다면 이를 통해 두 개의 구조체를 전송함으로써 여러 개의 단일함수 호출에 의한 낭비를 최소화시킨다.

 


백키 사용하기

  컨트롤에서 발생한 방향 키들은 가장 먼저 컨테이너의 메시지 큐를 통해 컨테이너가 처리하게 된다. 이때 컨트롤 포커스를 갖고 있는 경우라고 해도 중요한 역할을 하는 키를 IE 컨테이너에게서 받지 못하는 문제가 발생한다. 대표적인 키들이 바로 방향키와 탭 키인데, IE에서 매우 중요한 키이다. 물론 컨트롤들은 PreTranslateMessage 함수를 통해 이들 메시지를 가로 챌 수 있지만 이 함수 역시 항상 호출되지는 못한다. IE는 해당 컨트롤이 Active 상태에 있는 경우에만 이 함수를 호출해 준다.

 

BOOL CMyActiveXCtrl::PreTranslatemessage(MSG* pmsg)

{

  switch(pMsg->message)

  {

    case WM_KEYDOWN:

    case WM_KEYUP:

      switch (pMsg->wParam)

      {

        case VK_UP:

        case VK_DOWN:

        case VK_LEFT:

        case VK_RIGHT:

        case VK_HOME:

        case VK_END:

          SendMessage(pMsg->message, pMsg->wParam, pMsg->lParam);

          // Windowless 컨트롤들은 SendMessage를 호출할 수 없으므로

          // 여기서 직접 처리해야 한다.

          return TRUE;

       }

       break;

    }

    return COleControl::PreTranslateMessage(pMsg);

}

 

 만일 컨트롤이 차일드 윈도우를 가진 경우 다음 핸들러를 추가한다.

int CMyActiveXCtrl::OnMouseActivate(CWnd* pDesktopWnd,

                                    UINT nHitTest, UINT message)

{

   if(!m_bUIActive)

     OnActivateInPlace(TRUE, NULL);  // ==UI-Activate the Control

   return COleControl::OnMouseActivate(pDesktopWnd, nHitTest, message);

}

 

또한 HTML 문서가 읽혀질 때 컨트롤이 바로 Active 상태로 가야하는 경우에는 OnCreate에서 OnActivateInPlace 함수를 사용할 수 있다.

int CMyActiveXCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

  if(COleControl::OnCreate(lpCreateStruct) == -1)

     return -1;

  OnActivateInPlace(TRUE, NILL);  // == UI-Activate the control

  return 0;

}


사용자 삽입 이미지

 

Advanced Option 사용하기

윈도우를 하나 생성하는 것은 윈도우 OS에게는 매우 값비싼 행동이다. 따라서 윈도우 없이 컨트롤이 돌아갈 수 있다면 아주 빠르게 처리될 수 있을 것이다. 물론 이것은 컨트롤이 일반 윈도우가 아닌 InprocServer로 동작하기 때문에 가능한 일이다. 윈도우가 없는 컨트롤의 좀더 좋은 장점은 배경화면을 반드시 처리할 필요가 없다는 것과 반드시 사각형 영역을 책임지지 않아도 된다는 점이다. 이 경우 컨트롤의 모양은 원형이나 삼각형이 될 수도 있다.

  MFC 위저드로 컨트롤 프로젝트를 만드는 과정에서 Windowless activation 옵션을 선택하면 위저드는 다음 코드를 기본적으로 생성한다.

DWORD CMyCtrl::GetControlFlags()

{

  return COleControl::GetControlFlags() | windowlessAcitvate;

}

 

COleControl 클래스는 컨트롤이 윈도우를 갖지 않은 경우에도 마우스 캡처나 키보드 포커스 스크롤을 처리할 수 있도록, 다음 함수들의 동작을 지원한다.

 

GetFocus, SetFocus

GetCapture, SetCapture, ReleaseCapture

GetDC, ReleaseDC

InvalidateRgn

ScrollWindow

GetClientRect

 이 함수들은 컨테이너와 협력해 필요한 일을 정확히 수행해준다.

 

Unclipped device context(클리핑을 지원하지 않는 DC의 사용)

  windowless activation을 사용하지 않는 경우에 이 옵션을 사용할 수 있다. 컨트롤이 실제로 자신의 영역만을 처리한다면 이 옵션을 선택함으로써 COleControl 내부에서의 IntersectClipRect 함수 호출을 막아 속도를 향상시킬 수 있다. 이 옵션을 선택하면 다음 코드가 들어가게 된다.

DWORD CMyCtrl::GetControlFlags()

{

  return COleControl::GetControlFlags() & ~clipPaintDC;

}

 

Flicker-free activation(깜박임 없는 활성화 지원)

이 옵션 역시 Windowless activation 옵션을 사용하지 않는 경우에 사용할 수 있다. 만일 컨트롤이 Active 상태와 Inactive 상태에서 자신을 동일하게 그린다면, 이 옵션을 사용해 Active 상태와 Inactive 상태 사이에서 전환시 발생하는 Flicker(깜박임 현상)를 제거할 수 있다. 이 옵션을 사용하려면 다음과 같이 noFlickerActivate 플래그를 COleControl GetControlFlags에서 추가해 준다.

DWORD CMyCtrl::GetControlFlags()

{

  return COleControl::GetControlFlags() | noFlickerActivate;

}

 

Mouse point notifications when inactivation(비활성화 상태에서의 마우스 지원)

컨트롤이 Inactive 상태에 있을 때 WM_SETCURSOR 메시지와 WM_MOUSEMOVE를 처리해야 하는 경우가 있다. 이것은 COleControl IPointerInactive 인터페이스를 사용할 수 있게 함으로써 처리한다. MFC 위자드는 기본적으로 이것을 사용하지 않도록 생성되지만 다음 코드를 추가하면 사용할 수 있다.

DWORD CMyCtrl::GetControlFlags()

{

  return COleControl::GetControlFlags() | pointerInactive;

}

 

pointerInactive 옵션을 지정하면 COleControl IPointerInactive를 사용할 수 있게 되고 컨테이너는 자신에게 들어온 메시지의 좌표를 분석해 해당 컨트롤의 조표면 컨트롤의 메시지 맵으로 WM_SETCURSOR WM_MOUSEMOVE 메시지를 전달한다. 우리는 일반 윈도우처럼 메시지 맵에 메시지 핸들러를 추가해 처리할 수 있다. , 이때 멤버변수 m_hWnd NULL인지 꼭 체크하고 사용하도록 주의하자.

  때론 Inactive 상태에 있는 컨트롤이 OLE 드래그앤드롭의 목표가 되기를 원할지도 모른다. 그러나 OLE 객체가 드래드될 때 컨트롤은 Active 상태가 돼야 한다. 그러기 위해서는 COleControl::GetActivationPolicy 함수를 오버라이드하고 POINTERINACTIVE_ACTIVATEONDRAG 플래그를 반환해야 한다. 다음 코드를 보자.

DWORD CMyCtrl::GetActivationPolicy()

{

  return POINTERINACTIVE_ACTIVATEONDRAG;

}

 

이 코드는 IPointerInactive 인터페이스를 사용해 대부분의 경우에 마우스 메시지를 컨트롤이 처리하도록 만드는 예다. 그러나 컨테이너가 이 인터페이스를 지원하지 않는 경우, _dwMyOleMisc OLEMISC_ACTIVATEWHENVISIBLE을 추가해 컨트롤이 화면에 나타나는 순간 Active 상태로 가게 할 수도 있다. 하지만 만일 컨테이너가 IPointInactive 인터페이스를 지원한다면 이것은 대단히 소모적인 옵션이 된다. 이 두 가지 컨테이너를 모두 지원하기 위해 _dwMyOleMisc OLEMISC_IGNOREACTIVATEWHENVISIBLE를 추가한다.

 

static const DWORD BASED_CODE -dwMyOldeMisc =

 OLEMISC_ACTIVATEWHENVISIBLE |

 OLEMISC_IGNOREACTIVATEWHENVISIBLE |

 OLEMISC_SETCLIENTSITEFIRST |

 OLEMISC_INSIDEOUT|

 OLEMISC_CANTLINKINSIDE |

 OLEMISC_RECOMPOSEONRESIZE;

 

Optimized drawing code(화면처리시 최적화 지원)

어드밴스 옵션 중 가장 만만해 보이는 것이 이 옵션일 것이다. 하지만 막상 어떻게 옵티마이징하는지를 잘 모르는 경우가 많다. Inactive 상태에 있는 컨트롤이 컨테이너가 제공하는 디바이스 컨텍스트를 사용해 화면처리를 하는 경우, 컨트롤은 전형적인 GDI 객체 처리를 수행한다. GDI 객체(, 브러쉬 등)를 선택하고 이전 객체를 저장하고 다 사용한 후, 다시 이전 것을 선택하는 작업을 수행한다.

  컨테이너 안에 여러 개의 컨트롤이 존재하는 경우에 각각의 컨트롤이 GDI 객체 복원작업을 수행하는 것은 낭비가 된다. 각각의 컨트롤이 복원작업 없이 자신의 DC로 처리한 후, 마지막으로 컨테이너가 처음에 보관해 둔 GDI 객체로 복원해준다면 더 효율적이기 때문이다.

  컨테이너가 이러한 기능을 수행하고 있는지 판단하기 위해 COleControl IsOptimizedDraw 함수를 제공한다. 이 함수가 TRUE를 리턴한다면 현재 옵티마이징할 수 있는 상태라는 의미다. 그때는 앞서 선택한 GDI 객체를 복원하는 것을 무시해도 된다. 다음은 최적화하지 않은 OnDraw 함수다.

 

void CMyCtrl::OnDraw(CDC* pdc, CRect& rcBounds, CRect& rcInvalid)

{

  CPen pen(PS_SOLID, o, TranslateColor(GetForeColor()));

  CBrush brush(TranslateColor(GetBackColor()));

  CPen* pPenSave = pdc->SelectObject(&pen);

  CBrush* pBrushSave = pdc->SelectObject(&brush);

  Rectangle(rcBounds);

  pdc->SelectObject(pPenSave);

  pdc->SelectObject(pBrushSave);

}

 

이 함수 구현에서 pen brush OnDraw 함수가 호출될 때마다 생성되고 소멸된다. 이 두 변수를 클래스의 멤버로 선언하고 클래스 생성시 같이 생성하고 소멸시 같이 제거하면 좀더 효율적이다.

 

class CMyCtrl : public COleControl

{

   .

   .

   .

   .

  CPen m_pen;

  CBrush m_brush;

}

 

  이 변수를 사용해 OnDraw를 개선해 보자.

 

void CMyCtrl::OnDraw(CDC* pdc, CRect& rcBounds, CRect& rcInvalid)

{

  if(m_pen.m_hObject == NULL)

     m_pen.CreatePen(PS_SOLID, 0, TranslateColor(GetForeColor()));

  if(m_brush.m_hObject == NULL)

     m_brush.CreateSolidBrush(TranslateColor(GetBackColor()));

  CPen* pPenSave = pdc->SelectObject(&m_pen);

  CBrush* pBrushSave = pdc->SelectObject(&m_brush);

  Rectangle(rcBounds);

  pdc->SelectObject(pPenSave);

  pdc->SelectObject(pBrushSave);

}

 

이렇게 하면 OnDraw가 호출될 때마다 GDI 객체가 생성되지 않는다. 하지만 컨트롤의 ForeColor이나 BackColor 속성이 변경된다면 우리는 이 객체를 다시 생성해야 한다. OnForeColorChanged 함수와 OnBackColorChanged 함수를 오버라이드하자.

 

void CMyCtrl::OnForeColorChanged()

{

   m_pen.DeleteObject();

}

void CMyCtrl::OnBackColorChanged()

{

   m_brush.DeleteObject();

}

 

마지막으로 불필요한 SelectObject 함수 호출을 제거한다.

 

 

void CMyCtrl::OnDraw(CDC* pdc, CRect& rcBounds, CRect& rcInvalid)

{

  if(m_pen.m_hObject == NULL)

     m_pen.CreatePen(PS_SOLID, 0, TranslateColor(GetForeColor()));

  if(m_brush.m_hObject == NULL)

     m_brush.CreateSolidBrush(TranslateColor(GetBackColor()));

  CPen* pPenSave = pdc->SelectObject(&m_pen);

  CBrush* pBrushSave = pdc->SelectObject(&m_brush);

  Rectangle(rcBounds);

  if(! IsOptimizedDraw())

  {

    pdc->SelectObject(pPenSave);

    pdc->SelectObject(pBrushSave);

  }

}

 

  이렇게 해서 가장 최적화된 OnDraw 함수를 구현할 수 있다.

 

Loads Properties Asynchronously(비동기를 이용한 속성 읽기)

  이 옵션은 컨트롤에 ReadyState라는 스톡 프로퍼티와 ReadyStateChange 스톡 이벤트를 사용하게 해 준다. 이를 통해 컨트롤은 자신의 프로퍼티를 비동기로 받을 수 있다. 파일과 같은 큰 데이터가 속성으로 지정된 컨트롤의 경우 이를 비동기로 수신하고 ReadyState 프로퍼티와 ReadyStateChange 이벤트를 이용해 해당 속성의 로딩이 언제 시작됐고 언제 종료됐는지를 알 수 있다. 이 부분의 구현을 좀더 이해하려면 MSDN Inetnet First Steps: ActiveX Controls 문서를 참조하자.

 

IHTMLDocument2를 이용해 DOM에 접근하기

IE 4.0이 나오면서 컨트롤은 IHTMLDocument2 인터페이스를 이용해 좀더 풍부한 DHTML 객체에 접근할 수 있게 됐다. MS DHTML W3C DOM(Document Object Model)을 지원하는 DHTML 객체모델을 지원한다. 따라서 DHTML에서 자바스크립트로 각 객체에 접근하듯이 액티브X 컨트롤도 IHTMLDocument2 인터페이스를 이용해 각 객체에 접근할 수 있다.

  최근 필자는 프로젝트에서 IHTMLDocument2 인터페이스를 이용해 다른 프로임에 존재하는 타사에서 개발된 애플릿에 접근해 애플릿의 속성과 메쏘드를 호출함으로써 원하는 기능을 완성했다. 이렇듯 IHTMLDocument2 인터페이스는 액티브X 컨트롤이나 웹 브라우저 컨트롤을 사용하는 개발자에게 보다 막강한 능력을 부여해준다. 최근 IE 5.5 IE 6.0이 나오면서 IHTMLDocument3(IE 5.0부터 지원), IHTMLDocument4(IE 5.5), IHTMLDocument5(IE 6.0)까지 제공하고 있다.

  IHTMLDocument2 인터페이스에 대한 멤버함수들은 비주얼 스튜디오가 설치된 경우 설치폴더의 include 에서 찾을 수 있다.(Programfiles\Microsoft Visual Studio\vc98\include\mshtml.h) MSDN에서도 자세한 설명문서를 찾을 수 있을 것이다. IHTMLDocument2 인터페이스를 이용한 다음 두 가지 예를 살펴보자.

 

 

참고) 과거 IE 3.x에서의 컨트롤간 통신

  액티브X 컨트롤이 처음 나왔을 때 자바스크립트 엔진은 컨트롤들 사이를 중재해줄 만큼 충분한 기능을 갖고 있지 못했다. 컨트롤 간에 통신하는 유일한 방법은 OLE의 함수를 이용하는 것이었다. 그러나 이 방법에 대해 MS가 별다른 해법을 제시하지 않아, 당시 개발자에게는 넘기 힘든 과제가 됐었다. 그러나 다행히도 한 MS 개발자에 의해 컨트롤 관련 뉴스그룹에 배포된 몇 줄의 코드가 구세주가 돼, 비로소 컨트롤간 통신이 가능하게 된 일화가 있다.

  다음은 그때 코드의 일부인데, oleCtrl->GetClientSite()->GetContainer()->EnumObjects() 함수가 당시 구세주 역할을 했던 함수이다.

int GetData( COlecontrol * oleCtrl, CString ssObjName, CString * pStrRet)

{

  int nRet = -3;

  LPOLECONTAINER pContainer = NULL;

  // IOleContainer 인터페이스를 얻고

  HRESULT hr = oleCtrl->GetClientSite()->GetContainer(&pContainer);

  if(FAILED(hr))

  {

    TRACE("오류 - Container를 찾을 수 없습니다.\n");

    return -1;

  }

 

  // 임베드된 객체들의 집합을 얻고

  DWORD dwFlags = OLECONTF_EMBEDDINGS;

  LPENUMUNKNOWN pEnumUnknown = NULL;

 

  hr = pContainer->EnumObjects(dwFlags, &pEnumUnknown);

  if(FAILED(hr))

  {

    TRACE("오류 - Object list를 얻을 수 없습니다.\n");

    return -2;

  }

 

  // 임베드된 객체 중 원하는 객체를 찾는다.

  UPUNKONWN pNextControl = NULL;

  while(SUCCEEDED(hr) && pEnumUnknown->Next(1, &pNextControl, NULL) == S_OK)

  {

    LPDIPATCH pDispatch = NULL;

    // 컨트롤의 IDispatch 인터페이스를 얻는다.

    hr = pNextControl->QueryInterface(IID_IDispatch, (void**)&pDispatch);

    if(SUCCEEDed(hr))

    {

      // 원하는 오브젝트가 맞으면 GetData라는 메쏘드를 찾고 값을 얻는다.

      .....

      pDispatch->Release();

    }

    pNextControl->Release();

  }

  pEnumUnknown->Release();

 

  return nRet;

}

 

 

HTML 문서안의 DHTML 객체 탐색하기

  다음 리스트1, 2 MS에서 제공하는 walkall이라는 샘플을 약간 수정한 것이다.(밑에 참조) 마우스로 컨트롤을 클릭하면 전체 DHTML 객체를 참조하고 각 객체의 태그 이름을 보여준다.(자세한 소스는 이달의 디스켓 참조)

  리스트1 OnLButtonUp 핸들러에서 보듯이 컨트롤은 m_pClentSite->GetContainer()을 이용해 컨테이너의 인터페이스(IOleContainer)를 얻고, QueryInterface()를 사용해 IHTMLDocument2를 얻을 수 있다. 이 인터페이스를 얻으면 메쏘드를 사용할 수 있다. DHTML에서 사용하던 함수가 거의 그대로 제공된다. LookDocument() 함수에서는 get_all()을 사용해 DHTML Document 객체의 all 객체를 얻어왔다. all이라는 객체는 현재 문서 내의 모든 객체의 집합이므로 이를 통해 모든 객체에 접근할 수 있다. ShowCollection() 함수에서는 얻어진 all 객체에서 모든 객체를 찾아 태그이름을 저장하고 보여준다. 편의상 ATL에서 제공하는 몇 개의 클래스를 사용해 봤으며 OLE의 데이터형을 사용해도 좋다. 화면2는 만든 컨트롤을 HTML 문서에서 클릭했을 때의 화면이다.

사용자 삽입 이미지

 

액티브X 컨트롤을 찾아 대화하기

  이번에는 현지의 HTMl 문서에 있는 서로 다른 액티브X 컨트롤을 참조해 원하는 프로퍼티를 얻어오는 예 리스트3, 4를 살펴보겠다.(밑에 참조 및 이달의 디스켓 참조)

 

 

/* 리스트1 내용 시작 */

void CLookDHTMLCtrl::OnLButtonUp(UINT nFlags, CPoint point)

{

             // TODO: Add your message handler code here and/or call default

             IOleContainer* pContainer = NULL;

 

             //컨테이너의 인터페이스를 얻고

             m_pClientSite->GetContainer( &pContainer );

             if ( pContainer != NULL )

             {

                           IHTMLDocument2* pDoc = NULL;

                           //IID_IHTMLDocument2인터페이스를 얻고

                           pContainer->QueryInterface( IID_IHTMLDocument2, (void**)&pDoc );

                           LookDocument(pDoc);

                           pDoc->Release();

             }

             COleControl::OnLButtonUp(nFlags, point);

}

 

void CLookDHTMLCtrl::LookDocument(IHTMLDocument2* pDoc)

{

             IHTMLElementCollection* pElemColl = NULL;

             IHTMLElement* pElem = NULL;

             //전체 태그를 얻어온다

             HRESULT hr = pDoc->get_all(&pElemColl);

             if (SUCCEEDED(hr))

             {

                           // 각 태그를 표시

                           ShowCollection(pElemColl);

                           pElemColl->Release();

             }

}

 

#include <atlbase.h>

 

void CLookDHTMLCtrl::ShowCollection(IHTMLElementCollection* pElemColl)

{

    IDispatch* pElemDisp = NULL;

    IHTMLElement* pElem = NULL;

             HRESULT hr;

 

             long lCount=0;

             // 태그의 개수를 얻어온다.

             if (SUCCEEDED(hr = pElemColl->get_length( &lCount )))

             {

                           CComBSTR bstrAllTag;

                           for ( int i=0; i<lCount; i++ )

                           {

                                        VARIANT vIndex;

                                        vIndex.vt = VT_UINT;

                                        vIndex.lVal = i;

                                        VARIANT var2 = { 0 };

                                        LPDISPATCH pDisp;

 

                                        //각 태그의 IDispatch 인터페이스를 얻고

                                        if (SUCCEEDED(hr = pElemColl->item( vIndex, var2, &pDisp )))

                                        {

                                                     IHTMLElement* pElem = NULL;

                                                     //실재 IID_IHTMLElement를 얻고

                                                     if (SUCCEEDED(hr = pDisp->QueryInterface( IID_IHTMLElement, (LPVOID*)&pElem )))

                                                     {

                                                                  CComBSTR bstrTag;

                                                                  BSTR bstr;

 

                                                                  hr = pElem->get_tagName(&bstr);

                                                                  if (bstr)

                                                                  {

                                                                                bstrTag = bstr;

                                                                                ::SysFreeString(bstr);

                                                                  }

 

                                                                  // 객체가 이미지인 경우 이미지소스를 출력

                                                                  IHTMLImgElement* pImage = NULL;

                                                                  if (SUCCEEDED(hr = pDisp->QueryInterface( IID_IHTMLImgElement, (LPVOID*)&pImage )))

                                                                  {

                                                                                pImage->get_src(&bstr);

                                                                                if (bstr)

                                                                                {

                                                                                             bstrTag += " - ";

                                                                                             bstrTag += bstr;

                                                                                             SysFreeString(bstr);

                                                                                }

                                                                                pImage->Release();

                                                                  }

 

                                                                  bstrTag.Append("\n");

                                                                  bstrAllTag += bstrTag;

 

                                                                  pElem->Release();

                                                     } // QI(IHTMLElement)

                                                     pDisp->Release();

                                        } //if (SUCCEEDED(hr = pElemColl->item(...)))

                           } // for ( int i=0; i<lCount; i++ )

                           MessageBox(CString(bstrAllTag),"현재 HTML의 태그들");

             } // if (SUCCEEDED(hr = pElemColl->get_length(...)))

}

/* 리스트1 내용 끝 */

 

 

 

/* 리스트2 내용 시작 */

<html>

<head>

</head>

<body>

<OBJECT ID = "LookDHTML" WIDTH = 100 HEIGHT = 50

             classid = "clsid:74EE2342-87BA-49FA-B476-88EE17866A21"

             CODEBASE = "LookDhtml.cab#Version=1,0,0,1">

</OBJECT>

<br>

<br>

<image src="button_home.gif">

</body>

</html>

/* 리스트2 내용 끝 */

 

 

/* 리스트3 내용 시작 */

void CFindControlObjectCtrl::OnLButtonUp(UINT nFlags, CPoint point)

{

             // TODO: Add your message handler code here and/or call default

             IOleContainer* pContainer = NULL;

 

             //컨테이너의 인터페이스를 얻고

             m_pClientSite->GetContainer( &pContainer );

             if ( pContainer != NULL )

             {

                           IHTMLDocument2* pDoc = NULL;

                           //IID_IHTMLDocument2인터페이스를 얻고

                           pContainer->QueryInterface( IID_IHTMLDocument2, (void**)&pDoc );

                           //LookDocument(pDoc);

                           FindObject(pDoc);

                           pDoc->Release();

             }

            

             COleControl::OnLButtonUp(nFlags, point);

}

 

#include <atlbase.h>

void CFindControlObjectCtrl::FindObject(IHTMLDocument2* pDoc)

{

             IHTMLElementCollection* pElemColl = NULL;

             HRESULT hr = pDoc->get_all(&pElemColl);

             if (SUCCEEDED(hr))

             {

                           CComVariant vIndex(m_friendID); //객체들 중 친구를 찾고

                           CComVariant var2(0);

                           LPDISPATCH pDisp;

 

                           //각 태그의 IDispatch 인터페이스를 얻고

                           if (SUCCEEDED(hr = pElemColl->item( vIndex, var2, &pDisp )))

                           {

                                        //IHTMLObjectElement* pElem = NULL;

                                        IHTMLElement* pElem = NULL;

                                        //실재 IID_IHTMLElement를 얻고

                                        if (SUCCEEDED(hr = pDisp->QueryInterface( IID_IHTMLElement, (LPVOID*)&pElem )))

                                        {

                                                     DISPID dwDispID;

 

                                                     // 친구 객체의 ControlName을 얻는다.

                                                     USES_CONVERSION;

                                                     LPCOLESTR lpOleStr = T2COLE("ControlName");

                                                     if (SUCCEEDED( pDisp->GetIDsOfNames(IID_NULL, (LPOLESTR *)&lpOleStr, 1, 0, &dwDispID)))

                                                     {

                                                                  VARIANT var;

                                                                  DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};

                                                                  pDisp->Invoke(dwDispID, IID_NULL,

                                                                                             LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET,

                                                                  &dispparamsNoArgs, &var, NULL, NULL);

 

                                                                  CString msg = "My Friend's Name is "+CString(var.bstrVal);

                                                                  BSTR bstr;

                                                                  if (SUCCEEDED(pElem->get_id(&bstr)))

                                                                  {

                                                                                msg += "("+CString(bstr)+")";

                                                                  }

 

                                                                  MessageBox(msg, "친구 찾기");

                                                     }

 

                                                     pElem->Release();

                                        } // QI(IHTMLElement)

                                        pDisp->Release();

                           } //if (SUCCEEDED(hr = pElemColl->item(...)))

                           pElemColl->Release();

             }

}

/* 리스트3 내용 끝 */

 

 

/* 리스트4 내용 시작 */

<html>

<head>

</head>

<body>

<OBJECT ID = "Ctrl1" WIDTH = 250 HEIGHT = 50

             classid = "clsid:CCA9EE60-783B-419B-AF6F-DEC1D5775958"

             CODEBASE = "FindControlObject.cab#Version=1,0,0,1">

             <PARAM NAME="ControlName" VALUE="Tom">

             <PARAM NAME="FriendID" VALUE="Ctrl2">

</OBJECT>

<br>

<br>

<OBJECT ID = "Ctrl2" WIDTH = 250 HEIGHT = 50

             classid = "clsid:CCA9EE60-783B-419B-AF6F-DEC1D5775958"

             CODEBASE = "FindControlObject.cab#Version=1,0,0,1">

             <PARAM NAME="ControlName" VALUE="Jerry">

             <PARAM NAME="FriendID" VALUE="Ctrl1">

</OBJECT>

<br>

</body>

</html>

/* 리스트4 내용 끝 */

 

 

  리스트3에서의 OnLButtonUp 핸들로는 앞의 것과 동일하고, FindObject() 함수를 보면 1번의 예와는 다르게 인덱스를 이용해 원하는 객체에 접근하지 않고 ID 속성에 지정했던 값을 이용해 직접 접근하는 방식을 보여준다. 이렇게 각 컨트롤 객체에 대한 IDispatch 인터페이스를 얻고 Invoke() 함수를 호출해 컨트롤의 속성을 얻거나 지정하고 메쏘드를 호출할 수 있다. 화면3은 컨트롤을 클릭했을 때 다른 컨트롤과 통신 후 결과를 보여주는 화면이다. 이외에도 IHTMLDocument2 인터페이스 안에는 다양한 함수가 제공되고 있으므로 한 번씩 살펴보기 바란다.

사용자 삽입 이미지


 

다음호를 기대하며

  이번호에서는 IE의 구조와 컨트롤의 두 가지 상태에 대해 알아봤다. 여러분에게 유익한 정보가 됐기를 바란다. 특히 컨트롤이 DHTML 객체에 직접 접근할 수 있으므로 HTML 문서의 로직을 스크립트로 구현해 노출하지 않고도 많은 부분을 숨길 수 있다. 이러한 특성은 기능성 웹사이트를 만들 때 중요한 기술적 요소가 될 것이다.

  다음 호에서는 실제 컨트롤을 제작할 때 많이 사용하는 몇 가지 유용한 기능을 소개하겠다. 컨트롤을 개발하다 보면 기본적이지만 중요한 몇 가지 기능을 제공해야만 한다. 예를 들어 부드러운 화면처리라든지, 쓰레드를 사용하는 것, 한영모드 설정 등.... 이러한 몇 가지 필수기능에 대해 다뤄보겠다. 그럼 다음 호를 기대하며......