웹, HTML

Visual Studio 2005에서 브라우저 도우미 개체 만들기(Windows IETechCol)

디버그정 2008. 9. 6. 09:04

Visual Studio 2005에서 브라우저 도우미 개체 만들기(Windows IETechCol)

 

Tony Schreiner, John Sudds
Microsoft Corporation

2006년 10월 27일

요약: 이 기사에서는 Microsoft Visual Studio 2005를 사용하여 IObjectWithSite 인터페이스를 구현하고 Internet Explorer에 연결되는 구성 요소 개체 모델(COM) 개체인 단순 브라우저 도우미 개체(BHO)를 만드는 방법을 설명합니다. 초보적인 BHO를 만드는 방법을 단계별로 설명할 것입니다. 기사에서 작성할 BHO는 우선 Internet Explorer에서 문서를 로드할 때 "Hello World!"라는 메시지를 표시합니다. 그런 다음 BHO가 확장되어 로드된 페이지에서 이미지를 제거합니다. 이 기사는 브라우저의 기능을 확장하고 Internet Explorer용 웹 개발자 도구를 만드는 방법을 배우고자 하는 개발자를 위해 작성되었습니다.

목차

소개
개요
프로젝트 설정
기본 사항 구현
이벤트에 응답
DOM 조작
요약
관련 항목


소개

이 기사에서는 Microsoft Visual Studio 2005와 ATL(Active Template Library)을 사용하여 C++로 BHO를 개발합니다. ATL을 사용하기로 결정한 이유는 필요에 따라 확장할 수 있는 기본 구성 요소를 편리하게 구현할 수 있기 때문입니다. MFC(Microsoft Foundation Classes), Win32 API, COM 등의 다른 방법으로도 BHO를 만들 수 있지만 경량 라이브러리인 ATL을 사용하면 BHO 클래스 식별자(CLSID)의 레지스트리 설정을 비롯한 많은 세부 작업을 자동으로 처리할 수 있습니다.

ATL의 또 다른 장점은 COM 개체의 수명을 관리하는 COM 인식 스마트 포인터 클래스(CComPtr (영문)CComBSTR (영문) 등)입니다. 예를 들어 CComPtr은 값이 할당될 때 AddRef를 호출하며 개체가 소멸되거나 범위를 벗어나면 Release를 호출합니다. 스마트 포인터는 코드를 단순화하고 메모리 누수를 없애는 데 도움이 됩니다. 스마트 포인터의 안정성과 신뢰성은 단일 메서드 범위 내에서 사용될 때 특히 유용합니다.

이 기사의 처음 부분에서는 단순 BHO를 구현하고 Internet Explorer에 로드되었는지 확인하는 과정을 살펴보겠습니다. 다음 부분에서는 BHO를 브라우저 이벤트에 연결하는 방법을 알아보고 마지막 부분에서는 웹 페이지의 모양을 변경하는 DHTML 문서 개체 모델(DOM)과의 간단한 상호 작용을 살펴보겠습니다.


개요

브라우저 도우미 개체(BHO)의 정확한 정의는 무엇입니까? 간단히 말해 BHO는 Internet Explorer에 사용자 지정 기능을 추가하는 경량 DLL 확장입니다. 흔히 볼 수 있는 예는 아니며 본 기사도 초점은 아니지만 Windows Explorer 셸에 기능을 추가하는 데도 BHO를 사용할 수 있습니다.

BHO는 일반적으로 자체 사용자 인터페이스를 제공하지 않고 브라우저 이벤트와 사용자 입력에 응답하면서 백그라운드에서 작동합니다. 예를 들어 BHO는 팝업 차단 (영문), 자동 양식 채우기 또는 마우스 동작 (영문)에 대한 지원을 추가 등을 수행할 수 있습니다. 일반적으로 도구 모음 확장 (영문)에 BHO가 필요하다고 잘못 인식되고 있지만 도구 모음과 함께 BHO를 사용하면 보다 풍부한 사용자 환경을 제공할 수 있는 것은 사실입니다.

참고  BHO는 최종 사용자와 개발자 모두에게 편리한 도구이지만 BHO에는 브라우저와 웹 콘텐츠에 대한 상당한 권한이 부여되며 감지되지 않는 경우가 많기 때문에 사용자는 신뢰할 수 있는 곳에서만 BHO를 받고 설치하도록 충분한 주의를 기울여야 합니다.

BHO의 수명은 상호 작용하는 브라우저 인스턴스의 수명과 동일합니다. 즉, Internet Explorer 6 및 이전 버전에서는 각각의 새로운 최상위 창마다 새로운 BHO가 생성 및 소멸되지만 Internet Explorer 7에서는 각 탭마다 새로운 BHO가 생성되고 소멸됩니다. BHO는 WebBrowser 컨트롤 (영문)을 호스팅하는 다른 응용 프로그램이나 HTML 대화 상자와 같은 창에서는 로드되지 않습니다.

BHO 사용을 위한 주요 요구 사항은 IObjectWithSite (영문) 인터페이스 구현하는 것입니다. 이 인터페이스는 Internet Explorer와의 초기 통신을 원활히 하고 해제되기 전에 이를 BHO에 알리는 SetSite라는 메서드를 공개합니다. 이 인터페이스를 구현하고 BHO의 CLSID를 레지스트리에 추가하여 단순 브라우저 확장을 만들어 보겠습니다.

이제 시작합니다.


프로젝트 설정

Microsoft Visual Studio 2005에서 BHO 프로젝트를 만들려면
  1. 파일 메뉴에서 새 프로젝트...를 클릭합니다.
    새 프로젝트 대화 상자가 나타납니다. 이 대화 상자에는 Visual Studio에서 만들 수 있는 응용 프로그램 유형이 나열됩니다.
  2. Visual C++ 노드 아래에 "ATL"이 선택되어 있지 않으면 선택한 다음 Visual C++ 프로젝트 유형에서 "ATL 프로젝트"를 선택합니다. 프로젝트의 이름을 "HelloWorld"로 지정하고 기본 위치를 사용합니다. 확인을 클릭합니다.
  3. ATL 프로젝트 마법사에서 서버 유형이 "동적 연결 라이브러리(DLL)"인지 확인하고 마침을 클릭합니다.

DLL을 위한 기본 구성 요소가 생성됩니다. 이제 BHO를 구현하는 COM 개체를 추가합니다.

  1. 솔루션 탐색기 패널에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가 하위 메뉴에서 클래스...를 선택합니다.
  2. "ATL 단순 개체"를 선택하고 추가를 클릭합니다.
    ATL 단순 개체 마법사가 나타납니다.
  3. ATL 단순 개체 마법사이름에 약식 이름으로 "HelloWorldBHO"를 입력합니다.
    나머지 이름은 자동으로 채워집니다.
  4. ATL 단순 개체 마법사옵션에서 스레딩 모델에는 "아파트", 집합체에는 "아니요", 인터페이스 형식에는 "이중", 그리고 지원에는 "IObjectWithSite"를 각각 선택합니다.

    ATL 단순 개체 마법사 옵션
  5. 마침을 클릭합니다.

이 프로젝트의 일부로 다음과 같은 파일이 생성됩니다.

  • HelloWorldBHO.h - 이 헤더 파일에는 BHO에 대한 클래스 정의가 들어 있습니다.
  • HelloWorldBHO.cpp - 이 소스 파일은 프로젝트의 주 파일이며 COM 개체가 들어 있습니다.
  • HelloWorld.cpp - 이 소스 파일은 DLL을 통해 COM 개체를 제공하는 내보내기를 구현합니다.
  • HelloWorld.idl - 이 소스 파일은 사용자 지정 COM 인터페이스를 정의하는 데 사용할 수 있습니다. 본 기사에서는 이 파일을 변경하지 않습니다.
  • HelloWorld.rgs - 이 리소스 파일에는 DLL 등록 및 등록 취소 시에 작성 및 제거되는 레지스트리 키가 들어 있습니다.

기본 사항 구현

ATL 프로젝트 마법사SetSite의 기본 구현을 제공합니다. IObjectWithSite 인터페이스 계약에는 필요에 따라 반복해서 이 메서드를 호출할 수 있다는 것이 암시되어 있지만 Internet Explorer에서는 연결을 설정할 때와 브라우저를 종료할 때 정확하게 두 번 이 메서드를 호출합니다. 특히 이 예에서 BHO의 SetSite 구현에서는 다음과 같은 작업을 수행합니다.

  • 사이트에 대한 참조를 저장합니다. 초기화하는 동안 브라우저는 IUnknown 포인터를 최상위 WebBrowser 컨트롤로 전달하고 BHO는 이에 대한 참조를 전용 멤버 변수에 저장합니다.
  • 현재 유지하고 있는 사이트 포인터를 해제합니다. Internet Explorer에서 NULL을 전달하면 BHO는 모든 인터페이스 참조를 해제하고 브라우저에서 연결을 끊어야 합니다.

SetSite를 처리하는 과정에 BHO는 필요에 따라 다른 초기화 및 초기화 취소 작업을 수행해야 합니다. 예를 들어 브라우저 이벤트를 수신할 수 있도록 브라우저에 대한 연결 지점을 설정할 수 있습니다.


HelloWorldBHO.h

Visual Studio 솔루션 탐색기에서 HelloWorldBHO.h를 두 번 클릭하여 엽니다.

먼저 shlguid.h를 포함합니다. 이 파일은 IWebBrowser2 (영문) 및 프로젝트에서 나중에 사용되는 이벤트의 인터페이스 식별자를 정의합니다.

#include <shlguid.h>     // IID_IWebBrowser2, DIID_DWebBrowserEvents2 등(참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)

다음으로 CHelloWorldBHO 클래스의 공용 섹션에서 SetSite를 선언합니다.

STDMETHOD(SetSite)(IUnknown *pUnkSite);

STDMETHOD 매크로는 메서드를 가상으로 표시하고 공용 COM 인터페이스에 대한 올바른 호출 규칙을 가질 수 있도록 하는 ATL 규칙입니다. 이 매크로는 COM 인터페이스를 클래스에 있을 수 있는 다른 공용 메서드와 구분하는 데 도움이 됩니다. STDMETHODIMP 매크로는 멤버 메서드를 구현할 때 비슷하게 사용됩니다.

마지막으로 클래스 선언의 전용 섹션에서 브라우저 사이트를 저장할 멤버 변수를 선언합니다.

private:
    CComPtr<IWebBrowser2>  m_spWebBrowser;

HelloWorldBHO.cpp

이제 HelloWorldBHO.cpp로 전환하고 SetSite에 다음 코드를 삽입합니다.

STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
    if (pUnkSite != NULL)
    {
        // IWebBrowser2에 대한 포인터를 캐싱합니다.
        pUnkSite->QueryInterface(IID_IWebBrowser2, (void**)&m_spWebBrowser);
    }
    else
    {
        // 캐싱된 포인터 및 다른 리소스를 여기에서 해제합니다.
        m_spWebBrowser.Release();
    }

    // 기본 클래스 구현을 반환합니다.
    return IObjectWithSiteImpl<CHelloWorldBHO>::SetSite(pUnkSite);
}

초기화하는 동안 브라우저는 해당 최상위 IWebBrowser2 인터페이스에 대한 참조를 전달하며 코드에서 이 참조를 캐싱합니다. 초기화를 취소하는 동안 브라우저가 NULL을 전달합니다. 메모리 누수를 방지하고 순환 참조가 발생되지 않게 하려면 이때 모든 포인터와 리소스를 해제하는 것이 중요합니다. 마지막으로 나머지 인터페이스 계약을 이행할 수 있도록 기본 클래스 구현을 호출합니다.


HelloWorld.cpp

DLL이 로드되면 시스템은 DLL_PROCESS_ATTACH 알림과 함께 DllMain 함수를 호출합니다. Internet Explorer는 다중 스레딩을 활발하게 사용하므로 DllMain에 대한 잦은 DLL_THREAD_ATTACH 및 DLL_THREAD_DETACH 알림으로 인해 확장과 브라우저 프로세스의 전체 성능이 저하될 수 있습니다. 이 BHO에는 스레드 수준 추적이 필요 없으므로 DLL_PROCESS_ATTACH 알림 내에서 DisableThreadLibraryCalls (영문)를 호출하여 새 스레드 알림으로 인한 오버헤드가 발생하지 않도록 할 수 있습니다.

HelloWorld.cpp에서 다음과 같이 DllMain 함수 코드를 작성합니다.

extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        DisableThreadLibraryCalls(hInstance);
    }
    return _AtlModule.DllMain(dwReason, lpReserved); 
}

BHO 등록

이제 남은 일은 레지스트리에 BHO의 CLSID를 추가하는 것입니다. 이 항목은 DLL을 브라우저 도우미 개체로 표시하여 Internet Explorer가 시작할 때 BHO를 로드하도록 합니다. Visual Studio는 프로젝트를 빌드할 때 CLSID를 등록할 수 있습니다.

참고  Windows Vista에서는 레지스트리와 상호 작용하기 위해 Visual Studio에 상승된 권한이 필요합니다. 시작 메뉴에서 Microsoft Visual Studio 2005를 마우스 오른쪽 단추로 클릭하고 관리자로 실행을 선택하여 개발 환경을 시작하도록 하십시오.

이 BHO의 CLSID는 HelloWorld.idl의 다음과 같은 코드 블록에 나와 있습니다.

    importlib("stdole2.tlb");
    [
        uuid(D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77),
        helpstring("HelloWorldBHO Class")
    ]

이 파일에는 GUID가 세 개 있지만 우리에게 필요한 것은 라이브러리나 인터페이스 ID의 CLSID가 아닌 class의 CLSID입니다.

자동 등록 BHO를 만들려면
  1. Visual Studio의 솔루션 탐색기에서 HelloWorld.rgs를 엽니다.
  2. 파일 맨 아래에 다음 코드를 추가합니다.
    HKLM {
      NoRemove SOFTWARE {
        NoRemove Microsoft {   
          NoRemove Windows {
            NoRemove CurrentVersion {
              NoRemove Explorer {
                NoRemove 'Browser Helper Objects' {
                  ForceRemove '{D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77}' = s 'HelloWorldBHO' {
                    val 'NoExplorer' = d '1'
                  }
                }
              }
            }
          }
        }
      }
    }
  3. 위에서 ForceRemove 다음에 나오는 GUID를 HelloWorld.idl에 있는 BHO의 CLSID로 바꿉니다.
    중괄호는 바꾸지 마십시오.
  4. 파일을 저장하고 F6 키를 눌러 솔루션을 다시 빌드합니다.
    Visual Studio에서 개체를 자동으로 등록합니다.

NoRemove 키워드는 BHO 등록을 취소할 때 키를 삭제하지 않아야 한다는 것을 나타냅니다. 이 키워드를 지정하지 않으면 빈 키가 제거됩니다. ForceRemove 키워드는 키를 비롯하여 키에 포함된 모든 값과 하위 키를 삭제해야 한다는 것을 나타냅니다. ForceRemove는 또한 BHO를 등록할 때 키가 이미 있는 경우 다시 생성되도록 합니다.

이 BHO는 Internet Explorer용으로 특별히 설계되었기 때문에 Windows Explorer에서 로드하지 않도록 NoExplorer 값을 지정합니다. Windows Explorer는 NoExplorer 항목이 있으면 항목의 값이나 형식에 관계없이 BHO를 로드하지 않습니다.

이제 Visual Studio 2005의 빌드 메뉴에서 솔루션을 빌드할 수 있습니다.


테스트

빠른 테스트를 위해 SetSite에 중단점을 설정하고 F5 키를 눌러 디버거를 시작합니다. 디버그 세션에 사용할 실행 파일 대화 상자가 나타나면 "기본 웹 브라우저"를 선택하고 확인을 클릭합니다. Internet Explorer가 기본 브라우저가 아닌 경우 실행 파일을 찾을 수 있습니다.

참고   Windows Vista에서는 Internet Explorer의 보호 모드 기능이 별도의 프로세스를 시작하고 종료하므로 디버그가 약간 더 어렵습니다. 현재 세션에서 보호 모드를 해제하려면 Visual Studio와 같은 관리 프로세스에서 브라우저를 시작하거나 로컬 HTML 파일을 만들고 Internet Explorer에 대한 명령줄 매개 변수에서 파일을 지정하는 두 가지 손쉬운 방법을 사용할 수 있습니다.

브라우저는 시작할 때 BHO용 DLL을 로드합니다. 중단점에 도달하면 pUnkSite 매개 변수가 설정되어 있는지 확인합니다. F5 키를 다시 눌러 홈 페이지를 계속해서 로드합니다.

브라우저를 닫고 SetSite가 NULL을 사용하여 다시 호출되는지 확인합니다.


이벤트에 응답

이제 Internet Explorer에서 BHO를 로드하여 실행할 수 있음을 확인하였으므로 다음은 브라우저 이벤트에 반응하도록 BHO를 확장해 보겠습니다. 이 섹션에서는 ATL을 사용하여 페이지가 로드된 후에 메시지 상자를 표시하는 DocumentComplete (영문)에 대한 이벤트 처리기를 구현하는 방법을 설명합니다.

BHO는 이벤트에 대한 알림을 받기 위해 브라우저와 연결 지점을 설정하며 이러한 이벤트에 응답할 수 있도록 IDispatch (영문)를 구현합니다. DocumentComplete에 대한 설명서에 따르면 이벤트에는 pDisp(IDispatch에 대한 포인터) 및 pUrl이라는 매개 변수 두 개가 있습니다. 이러한 매개 변수는 이벤트의 일부로 IDispatch::Invoke에 전달됩니다. 그러나 이벤트 매개 변수를 직접 처리하는 것은 어렵고 오류가 발생하기 쉬운 작업입니다. 다행스럽게도 ATL은 이벤트 처리 논리를 단순화하는 데 도움이 되는 기본 구현을 제공합니다.


HelloWorldBHO.h

HelloWorldBHO.h는 브라우저 이벤트의 발송 ID를 정의하는 exdispid.h를 포함하는 것으로 시작합니다.

#include <exdispid.h> // DISPID_DOCUMENTCOMPLETE 등

다음으로 이벤트 처리를 위한 Invoke의 쉽고 안전한 대안이 되는 IDispEventImpl 기본 클래스에서 파생시킵니다. IDispEventImpl은 이벤트 싱크 맵과 함께 작업하여 적절한 처리기 함수로 이벤트를 라우팅합니다. 다음 클래스 정의(강조 표시됨)를 사용하여 DWebBrowserEvents2 인터페이스에 의해 정의되는 이벤트를 처리하도록 지정합니다.

class ATL_NO_VTABLE CHelloWorldBHO :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHelloWorldBHO, &CLSID_HelloWorldBHO>,
    public IObjectWithSiteImpl<CHelloWorldBHO>,
    public IDispatchImpl<IHelloWorldBHO, &IID_IHelloWorldBHO, &LIBID_TestBho1Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispEventImpl<1, CHelloWorldBHO, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>

다음으로 새 OnDocumentComplete 이벤트 처리기 메서드로 이벤트를 라우팅하는 ATL 매크로를 추가합니다. 이 메서드는 DocumentComplete 이벤트에서 정의한 것과 동일한 인수를 동일한 순서로 받습니다. 클래스의 공용 섹션에 다음 코드를 넣습니다.

BEGIN_SINK_MAP(CHelloWorldBHO)
    SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
END_SINK_MAP()

    // DWebBrowserEvents2
 void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL); 

SINK_ENTRY_EX 매크로에 제공된 수(1)는 IDispEventImpl 클래스 정의의 첫 번째 매개 변수를 나타내며 필요한 경우 다른 인터페이스에서 이벤트를 구분하는 데 사용됩니다. 이벤트 처리기에서는 값을 반환할 수 없다는 사실도 기억해야 합니다. 그러나 Internet Explorer는 Invoke에서 반환하는 값을 무시하므로 관계는 없습니다.

마지막으로 개체가 브라우저와 연결을 설정했는지 여부를 추적하는 전용 멤버 변수를 추가합니다.

private:
    BOOL m_fAdvised; 

HelloWorldBHO.cpp

이벤트 맵을 통해 브라우저에 이벤트 처리기를 연결하려면 SetSite를 처리하는 동안 DispEventAdvise (영문)를 호출합니다. 마찬가지로 연결을 끊으려면 DispEventUnadvise (영문)를 사용합니다.

다음은 SetSite의 새 구현입니다.

STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
    if (pUnkSite != NULL)
    {
        // IWebBrowser2에 대한 포인터를 캐싱합니다.
        HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser);
        if (SUCCEEDED(hr))
        {
            // DWebBrowserEvents2에서 이벤트 싱크를 등록합니다.
            hr = DispEventAdvise(m_spWebBrowser);
            if (SUCCEEDED(hr))
            {
                m_fAdvised = TRUE;
            }
        }
    }
    else
    {
        // 이벤트 싱크 등록을 취소합니다.
        if (m_fAdvised)
        {
            DispEventUnadvise(m_spWebBrowser);
            m_fAdvised = FALSE;
        }

        // 캐싱된 포인터 및 다른 리소스를 여기에서 해제합니다.
        m_spWebBrowser.Release();
    }

    // 기본 클래스 구현을 호출합니다.
    return IObjectWithSiteImpl<CHelloWorldBHO>::SetSite(pUnkSite);
}

마지막으로 간단한 OnDocumentComplete 이벤트 처리기를 추가합니다.

void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
    // 사이트에서 최상위 창을 검색합니다.
    HWND hwnd;
    HRESULT hr = m_spWebBrowser->get_HWND((LONG_PTR*)&hwnd);
    if (SUCCEEDED(hr))
    {
        // 페이지가 로드되면 메시지 상자를 출력합니다.
        MessageBox(hwnd, L"Hello World!", L"BHO", MB_OK);
    }
}

메시지 상자에서는 해당 매개 변수에 단순히 NULL을 전달하지 않고 사이트의 최상위 창을 부모 창으로 사용합니다. Internet Explorer 6에서 NULL 부모 창은 응용 프로그램을 차단하지 않으므로 메시지 상자에서 사용자 입력을 대기하는 동안 사용자가 계속해서 브라우저와 상호 작용할 수 있습니다. 때로는 이로 인해 브라우저가 정지하거나 충돌할 수 있습니다. 드문 경우이기는 하지만 BHO가 UI를 표시해야 하는 경우도 있으므로 부모 창에 대한 핸들을 지정하여 항상 메시지 상자가 응용 프로그램 모달이 되도록 해야 합니다.


다른 테스트 해보기

F5 키를 눌러 Internet Explorer를 다시 시작합니다. 문서가 로드되면 BHO가 다음 메시지를 표시합니다.

"Hello World!" 메시지 상자

탐색을 계속하면서 메시지 상자가 언제 얼마나 자주 나타나는지 관찰합니다. BHO 알림은 페이지가 로드될 때뿐 아니라 뒤로 단추를 클릭하여 페이지를 다시 로드하는 경우에도 표시됩니다. 그러나 새로 고침 단추를 클릭할 때는 표시되지 않습니다. Internet Explorer 7에서는 새 탭마다 메시지 상자가 표시됩니다.

이벤트는 페이지를 다운로드하고 구문 분석을 마친 후 window.onload (영문) 이벤트가 트리거되기 전에 발생합니다. 프레임이 여러 개 있는 경우 이벤트가 여러 번 발생하고 마지막에 최상위 프레임 이벤트가 발생합니다. 다음 코드에서는 이벤트의 pDisp 매개 변수에 전달된 개체와 SetSite에 캐싱된 최상위 브라우저를 비교하여 일련의 최종 이벤트를 검색했습니다.


DOM 조작

다음 JavaScript 코드는 DOM의 기본 조작을 보여 줍니다. 이 코드에서는 이미지의 스타일 개체의 display (영문) 특성을 "none"으로 설정하여 웹 페이지에서 이미지를 감춥니다.
function RemoveImages(doc)
{
    var images = doc.images;
    if (images != null)
    {
        for (var i = 0; i < images.length; i++) 
        {
            var img = images.item(i);
            img.style.display = "none";
        }
    }
}

마지막 섹션에서는 C++로 이 기본 로직을 구현하는 방법을 보여 줍니다.


HelloWorldBHO.h

우선 HelloWorldBHO.h를 열고 mshtml.h를 포함합니다. 이 헤더 파일은 DOM을 사용하기 위해 필요한 인터페이스를 정의합니다.

#include <mshtml.h>         // DOM 인터페이스

다음으로 위의 JavaScript C++ 구현을 포함하도록 전용 멤버 메서드를 정의합니다.

private:
    void RemoveImages(IHTMLDocument2 *pDocument);

HelloWorldBHO.cpp

OnDocumentComplete 이벤트 처리기는 이제 다음과 같은 두 가지 새 작업을 수행합니다. 첫 번째는 캐싱한 WebBrowser 포인터를 이벤트가 발생한 개체와 비교하는 것이며 두 항목이 같으면 해당 이벤트는 최상위 창에 대한 이벤트이며 문서는 완전히 로드된 것입니다. 두 번째는 document (영문) 개체에 대한 포인터를 검색하고 RemoveImages로 전달하는 것입니다.

void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
    HRESULT hr = S_OK;

    // IWebBrowser2 인터페이스에 대해 쿼리합니다.
    CComQIPtr<IWebBrowser2> spTempWebBrowser = pDisp;

    // 이벤트가 최상위 창과 연결되어 있습니까?
    if (spTempWebBrowser && m_spWebBrowser &&
        m_spWebBrowser.IsEqualObject(spTempWebBrowser))
    {
        // 브라우저에서 현재 문서 개체를 얻습니다...
        CComPtr<IDispatch> spDispDoc;
        hr = m_spWebBrowser->get_Document(&spDispDoc);
        if (SUCCEEDED(hr))
        {
            // ...그리고 HTML 문서에 대해 쿼리합니다.
            CComQIPtr<IHTMLDocument2> spHTMLDoc = spDispDoc;
            if (spHTMLDoc != NULL)
            {
                // 마지막으로 이미지를 제거합니다.
                RemoveImages(spHTMLDoc);
            }
        }
    }
}

pDisp의 IDispatch 포인터에는 문서가 로드된 창이나 프레임의 IWebBrowser2 인터페이스가 포함되어 있습니다. 이 값을 QueryInterface를 자동으로 수행하는 CComQIPtr 클래스 변수로 저장합니다. 다음으로 페이지가 완전히 로드되었는지 확인하기 위해 인터페이스 포인터와 최상위 브라우저의 SetSite에 캐싱한 포인터를 비교합니다. 이 테스트 결과 최상위 브라우저 프레임의 문서에서만 이미지를 제거하게 됩니다. 최상위 프레임으로 로드되지 않은 문서는 이 테스트를 통과하지 못합니다. 자세한 내용은 How To Determine When a Page Is Done Loading in WebBrowser ControlHow to get the WebBrowser object model of an HTML frame을 참조하십시오.

HTML document 개체를 검색하려면 두 단계를 거쳐야 합니다. get_Document는 브라우저가 다른 형식(예: Microsoft Word 문서)의 문서 개체를 호스팅한 경우에도 활성 문서에 대한 포인터를 검색하므로 IHTMLDocument2 (영문) 인터페이스에 대한 활성 문서를 추가로 쿼리하여 실제 HTML 페이지인지 확인해야 합니다. IHTMLDocument2 인터페이스는 DHTML DOM의 콘텐츠에 대한 액세스를 제공합니다.

HTML 문서가 로드된 것을 확인한 후에는 RemoveImages로 값을 전달합니다. 인수는 CComPtr이 아닌 IHTMLDocument2에 대한 포인터로 전달됩니다.

void CHelloWorldBHO::RemoveImages(IHTMLDocument2* pDocument)
{
    CComPtr<IHTMLElementCollection> spImages;

    // DOM에서 이미지 컬렉션을 얻습니다.
    HRESULT hr = pDocument->get_images(&spImages);
    if (hr == S_OK && spImages != NULL)
    {
        // 컬렉션에서 이미지 수를 얻습니다.
        long cImages = 0;
        hr = spImages->get_length(&cImages);
        if (hr == S_OK && cImages > 0)
        {
            for (int i = 0; i < cImages; i++)
            {
                CComVariant svarItemIndex(i);
                CComVariant svarEmpty;
                CComPtr<IDispatch> spdispImage;

                // 컬렉션에서 인덱스로 이미지를 얻습니다.
                hr = spImages->item(svarItemIndex, svarEmpty, &spdispImage);
                if (hr == S_OK && spdispImage != NULL)
                {
                    // 먼저 제네릭 HTML 요소 인터페이스에 대해 쿼리합니다...
                    CComQIPtr<IHTMLElement> spElement = spdispImage;

                    if (spElement)
                    {
                        // ...다음은 스타일 인터페이스에 정보를 요청합니다.
                        CComPtr<IHTMLStyle> spStyle;
                        hr = spElement->get_style(&spStyle);

                        // display="none"으로 설정하여 이미지를 감춥니다.
                        if (hr == S_OK && spStyle != NULL)
                        {
                            static const CComBSTR sbstrNone(L"none");
                            spStyle->put_display(sbstrNone);
                        }
                    }
                }
            }
        }
    }
}

C++로 DOM과 상호 작용하려면 JavaScript보다 더 복잡하지만 코드 흐름은 기본적으로 동일합니다.

앞의 코드는 이미지 컬렉션의 각 항목마다 반복됩니다. 스크립트에서는 컬렉션 요소가 서수나 이름 중 어느 것으로 액세스되는지 확실하지만 C++에서는 빈 variant를 전달하여 이러한 인수를 수동으로 구분해야 합니다. 여기에서도 ATL 도우미 클래스(이번에는 CComVariant)를 사용하여 작성해야 할 코드의 양을 줄였습니다.


최종 참고 사항

스크립트 작성을 용이하게 하기 위해 DOM의 모든 개체는 IDispatch를 사용하여 여러 인터페이스에서 파생된 속성과 메서드를 공개합니다. 그러나 C++에서는 사용할 속성이나 메서드를 지원하는 인터페이스를 명시적으로 쿼리해야 합니다. 예를 들어 이미지 개체는 IHTMLElement (영문)IHTMLImgElement (영문) 인터페이스를 모두 지원합니다. 그러므로 이미지에 대한 스타일 개체를 검색하려면 우선 get_style 메서드를 제공하는 IHTMLElement 인터페이스를 쿼리해야 합니다.

또한 COM 규칙은 실패 시에 유효한 포인터를 보장하지는 못하므로 모든 COM 호출 후에는 HRESULT를 확인해야 합니다. 더욱이 많은 DOM 메서드의 경우 NULL 값을 반환하는 것이 오류가 아니기 때문에 반환 값과 포인터 값을 모두 주의해서 확인해야 합니다. 보다 정확한 확인을 위해 항상 미리 포인터를 NULL로 초기화하십시오. 방어적이고 자세하며 오류를 방지하는 코딩 스타일을 채택하면 나중에 예기치 못한 버그를 예방하는 데 도움이 됩니다.


요약

광범위한 용도로 사용되는 다양한 BHO가 있지만 모든 BHO에는 하나의 공통된 기능이 있습니다. 브라우저에 대한 연결 기능입니다. Internet Explorer와 긴밀하게 통합될 수 있기 때문에 BHO는 브라우저의 기능을 확장하려는 많은 개발자들이 중요하게 여기는 도구입니다. 이 기사에서는 로드된 문서에서 IMG 요소의 스타일 특성을 수정하는 단순 BHO를 작성하는 방법을 설명했습니다. 이 초급적인 예를 원하는 대로 확장해 보시기 바랍니다. 다음 링크를 방문하여 확장 가능성을 더 자세히 연구해 볼 수 있습니다.


관련 항목