PHP & MySQL

팝업방지 BHO

관리자 | 2013.02.28 14:54 | 조회 9317
오늘은 인터넷 익스플로러에서 팝업 창이 뜨지 않도록 차단하는 모듈 개발에 대한 강좌를 하도록 하겠습니다.
이번 강좌를 통해서 COM (Component Object Model)의 이벤트 매커니즘 및 IDispatch 인터페이스, 듀얼 인터페이스에 대해서 조금이나마 익힐 수 있는 계기가 되었으면 하는 바램입니다.

우리가 최종적으로 개발할 것은 BHO(Browser Helper Object) 방식으로 인터넷 익스플로러에 탑재되어서 특정 웹 사이트로 이동하거나 새창이 뜨는 것을 차단하는 것입니다. 하지만 이번 강좌가 단순히 웹 사이트 차단하는 소스를 알려주는 것이 아니라 COM의 이벤트 매커니즘을 공부하는 것이기 때문에, 처음부터 BHO 모듈을 만들지 않고 테스트 프로그램을 먼저 만들어서 COM의 이벤트 매커니즘을 먼저 공부한 후에 BHO 모듈 개발 방법 등에 대해서 차례 차례 배우도록 하겠습니다.

서두부터 COM 이니 BHO 니 하는 용어들을 썻다고 해서 너무 어렵게 생각하지 말고 한번 따라하면서 강좌를 읽어보세요.

Part 1. WebBrowser 컨트롤 사용해 보기

우선, 간단하게 테스트 프로그램을 하나 만들어서 COM의 이벤트 매커니즘에 대해서 알아보겠습니다. Part1 예제 소스는 여기(1st_webbrowser_control.zip)있습니다.

C++ 빌더 2009를 실행해서 VCL Forms Application 프로젝트를 생성하세요.

01_새프로젝트.jpg

그리고 아래와 같이 메인폼을 간단하게 디자인합니다. 폼의 상단에는 TLabeledEdit ('edUrl'' 로 이름 지었습니다)로 이동할 주소를 입력 받을 수 있도록 하고, 하단에는 TWebBrowser 컨트롤('wb' 로 이름 지었습니다)을 넣었습니다.

02_메인화면설계.jpg

그리고 에디터 상자에서 Enter 키를 누르거나 "이동" 버튼을 누르면 사용자가 입력한 주소로 이동 할 수 있도록 아래와 같이 코딩합니다.

01.
02.bool TForm1::Navigate(void)
03.{
04. if (edUrl->Text == "")
05. return false;
06. //에디트 상자에 입력된 주소로 이동한다.
07. wb->Navigate(edUrl->Text);
08. return true;
09.}
10.//---------------------------------------------------------------------------
11.void __fastcall TForm1::edUrlKeyUp(TObject *Sender, WORD &Key, TShiftState Shift)
12.{
13. //엔터키를 눌렀으면 에디터 상자에 입력된 주소로 이동한다.
14. if (Key == VK_RETURN)
15. {
16. Navigate();
17. }
18.}
19.//---------------------------------------------------------------------------
20.void __fastcall TForm1::btnNavigateClick(TObject *Sender)
21.{
22. Navigate();
23.}
24.//---------------------------------------------------------------------------

다 만들었으면 이제 실행해볼까요?

03_테스트프로그램실행화면.jpg
주소창에 www.naver.com 을 입력했더니 위와 같이 잘되네요...ㅋㅋ

이왕 만들었으니 아예 인터넷 익스플로러 처럼 비슷하게 한번 만들어 보는 것은 어떨까요?
우선, 인터넷 익스플로러의 상태창부터 한번 흉내내 봅시다.

인터넷 익스플로러의 하단에 있는 상태창을 보면 웹 페이지에서 링크위에 마우스가 있을때 해당 링크의 주소도 표시하고 이미지나 HTML 파일을 다운로드 받을때에는 진행바가 표시되어서 얼만큼 수신했는지도 표시합니다.
어떻게 구현해야 할까요?

웹 브라우저 컨트롤에서 뭔가 제공하지 않을까요? 단서를 찾아야하니, 우리가 만든 테스트 프로그램에서 웹 브라우저 컨트롤을 선택한 다음 Object Inspector 에서 이벤트 탭을 한번 봅시다. 뭔가 단서가 있나요? 자세히 한번 봅시다.
OnProgress, OnStatusTextChange, OnNewWindow2, OnBeforeNavigate2... 이벤트가 참 많네요...
그런데, OnStatusTextChange ?

뭔가 확실한 단서를 찾은것 같습니다. 그냥 둘수 없습니다. 어떤 놈인지 더블 클릭해서 확인해 봅시다.
04_웹브라우저컨트롤이벤트목록.jpg

더블 클릭 했더니 아래와 같이 이벤트 핸들러가 생성되었습니다. 이 이벤트의 파라메터를 자세히 보니 WideString Text 라는 파라메터가 있네요...

1.
2.void __fastcall TForm1::wbStatusTextChange(TObject *ASender, const WideString Text)
3.{
4.}

우리가 찾던 놈이 맞는 것 같습니다. Text 파라메터 값을 캡션에 한번 찍어봅시다. 아래와 같이 말이죠...ㅋㅋ

1.
2.void __fastcall TForm1::wbStatusTextChange(TObject *ASender, const WideString Text)
3.{
4. //웹 브라우저의 현재 상태를 캡션에 출력합니다.
5. Caption = Text;
6.}

한번 실행해 보세요. 아래와 같이 폼의 상단 캡션에 웹 브라우저 컨트롤이 하고 있는 작업이 표시되나요?
별거 아니네요...ㅋㅋ
05_상태문자열표시화면.jpg

대충 감이 오셨습니까? TWebBrowser 컨트롤의 이벤트 목록을 보면 그 이름으로 대충 어떤 의미인지를 알수있습니다. 즉, 웹 브라우저 컨트롤에서 발생하는 대부분의 이벤트 (새창 열기, 문서 다운로드, 오류 상황 등)를 프로그램에서 감지하고 발생한 이벤트에 대한 처리를 할 수 있습니다.

그럼 이제 우리가 이 강좌에서 하기로한 팝업을 차단하는 기능을 한번 추가해 볼까요? 아까처럼 다시 웹 브라우저 컨트롤을 선택하고 속성 창에서 이벤트 목록을 한번 봅시다. 팝업을 차단하려면 어떤 이벤트에서 처리해야할까요?
이제는 금방 아시겠죠? 네, 맞습니다. OnNewWindow2 이벤트에서 처리하면 됩니다. 그러면 한번 더블 클릭해서 이벤트의 형태를 한번 살펴 봅시다.

1.
2.void __fastcall TForm1::wbNewWindow2(TObject *ASender, IDispatch *&ppDisp, WordBool &Cancel)
3.{
4.}

위와 같이 나오셨나요? 그럼 OnNewWindow2 라는 이벤트를 통해서 전달되는 파라메터가 어떤 것들인지 한번 볼까요?
마지막 파라메터로 Cancel 이라는 파라메터가 참조 형태로 넘어오네요. 네, 생각하신대로 입니다. 이 파라메터를 true로 설정하면 새창이 열리지 않습니다. 한번 해볼까요? 아래와 같이 작성하세요.

1.
2.void __fastcall TForm1::wbNewWindow2(TObject *ASender, IDispatch *&ppDisp, WordBool &Cancel)
3.{
4. Cancel = true;
5.}

저는 아래 그럼처럼 cjmall을 들어가 봤는데 팝업이 전혀 뜨지 않네요. 어떤가요? 생각보다 팝업 윈도우가 뜨지 않도록하는 것은 쉽지 않나요?
아직 웹 브라우저에 탑재된 형태로 개발하지는 않았지만 팝업을 차단하는 기본적인 방법은 익혔으니 절반은 했네요. ㅎㅎ

cjmall_팝업차단화면.jpg

다음으로 진행하기전에 머리도 식힐겸 잡담좀 하겠습니다 (사실은 잡담이 아니라 들으시면 머리가 더 아파올 수 있는 기술적인 이야기 입니다. ㅎㅎ).

C++ 빌더로 엑티브엑스를 임포트해 보신적 있으신가요? 해보신분은 아시겠지만 빌더가 자동으로 엑티브엑스가 가지고 있는 속성 및 이벤트를 정의한 C++ 파일을 생성해줍니다. 마치 VCL인 것처럼, 예전부터 있던 컴포넌트 처럼 컴포넌트 파렛트에 아이콘까지 생깁니다. 그리고 다른 VCL 컴포넌트들 처럼 폼위에 올려놓고 이벤트 탭에서 더블 클릭해서 이벤트 핸들러를 작성할 수 있습니다.

신기하다는 생각하신적 없으신가요? 저는 처음 이런 기능을 봤을때 너무 신기하고 궁금해서 잠이 오지 않았습니다 (개발에 관해서 모르는 것이나 궁금한 것이 있으면 알때까지 잠이 잘 않오는 성격이라서..-_-;).
어떻게 구현한 것일까요? 어떻게 남이 만든 엑티브엑스 컨트롤에 있는 이벤트와 속성을 그렇게 잘 알고 있는 것일까요?

바로 "타입 라이브러리" 라는 것이 있기 때문입니다. 타입 라이브러리가 무엇인가 하면, COM 서버가 제공하고 있는 인터페이스와 인터페이스의 상속 구조, 메서드, 타입, 각 메서드의 파라메터 및 타입 등 COM 서버에서 제공하는 모든 타입들에 대한 자세한 정보가 들어 있는 바이너리 형태의 모듈입니다.
이 타입 라이브러리는 *.tlb 형태로 분리되어서 생성될 수 도 있고 COM 서버에 통합될 수도 있는데, C++ 빌더로 만든 COM 서버는 보통 COM 서버에 통합된 형태로 만들어 집니다.
COM이 처음 소개되고 툴에서 지원하는 기능이 미약한 시절에는 IDL (Interface Definition Language) 이라는 인터페이스 정의 언어로 COM 서버에서 사용하는 인터페이스를 정의하고 IDL 컴파일러로 컴파일하는 방법을 사용했지만 요즘은 툴에서 대부분 자동으로 생성하기 때문에 모르는 사람이 더 많습니다.
그 만큼 개발자들은 더 편하게 고급 기술을 사용할 수 있게 되었지만, 반대로 COM에 대한 지식의 수준이 점점더 얕아져서 단순한 문제도 어떻게 해결해야하는지 모르는 시대가 된거죠...

암튼, 이 타입 라이브러리를 이용해서 C++ 빌더가 엑티브엑스에 있는 이벤트 나 속성에 대한 정보를 얻어서 자동으로 C++ 파일을 생성한다는 것을 알려 드리고 싶어서 이렇게 얘기가 길어졌네요...

다음에 스크립트 언어와 타입 라이브러리를 이용해서 COM 객체를 스크립트로 제어하는 기술에 대해서 강좌를 쓸 생각인데, 그때 타입 라이브러리에 대해서 더 자세히 말씀드리도록 하고 타입 라이브러리에 대한 얘기는 이것으로 마치겠습니다.

Part 2. BHO 만들기

그러면 이제부터는 실제 BHO 모듈을 만들어서 웹 브라우저에 탑재시킨뒤 팝업을 차단하는 방법에 대해서 강의를 계속 이어 나가도록 하겠습니다. Part2 예제 소스는 여기("2nd_bho.zip") 있습니다.

BHO 라는 용어 들어 보셨나요? BHO 란 Browser Helper Object 의 줄임말입니다. 한글로하면 브라우저 도우미 객체가 되겠네요.
BHO가 무엇이냐면, 인터넷 익스플로러의 모자란 기능을 개발자가 직접 만들어서 확장시킬 수 있도록 하기 위해서 마이크로 소프트에서 정의한 기술입니다.
BHO는 COM 서버 형태로 개발합니다. 그리고 레지스트리에 지정된 위치에 개발한 BHO에 대한 정보를 등록하면 인터넷 익스플로러가 실행될때 BHO 모듈을 로드해서 인터넷 익스플로러의 기능을 확장할 수 있게 됩니다.

자 그럼 BHO에 대해서 마이크로소프트에서는 뭐라고 하는지 인터넷으로 한번 살펴봅시다. 아래의 링크를 클릭해보면 BHO에 관한 MSDN 자료가 있습니다.
(http://msdn.microsoft.com/en-us/library/bb250436.aspx)
링크에 있는 것은 영문 자료입니다. "할수있다! 개발자영어" 게시판에 번역해 놓을 테니 참고하시구요. 그럼, BHO가 무엇이며 어떻게 개발하는지에 대해서 MSDN 자료를 한번 살펴보겠습니다.

MSDN 자료를 보면, BHO 는 Browser Helper Object의 약자로 인터넷 익스플로러를 커스터마이징 할 수 있도록 하기 위해서 MS에서 만든 기술입니다. BHO는 기본적으로 In-Process 타입의 COM 서버 형태로 개발해야하며 지정된 레지스트리 경로에 여러분이 개발한 BHO에 대한 정보를 등록해야 인터넷 익스플로러가 인식하도록 되어 있습니다.

그러면 이제부터 MSDN의 자료를 바탕으로 BHO 모듈을 만들어보도록 하겠습니다.

1.프로젝트 생성

첫번째로 BHO는 COM 서버 형태로 개발해야합니다.
C++빌더를 켜시고 아래와 같이 ActiveX 프로젝트를 생성하세요.
ActiveX Library project.jpg

그리고 나서, 실제 BHO 역할을 할 COM 서버 객체를 프로젝트에 추가해야합니다.
"File > New > Other" 매뉴를 선택하시고 아래와 같이 COM Object 를 추가하세요.
add_com_object.jpg

그러면 아래와 같은 화면이 나올텐데, 여기에 우리가 만들 팝업 차단 BHO의 적당한 이름을 입력하세요. 저는 "PopupBlocker"라고 입력했습니다.
wizard_1.jpg

여기서 Threading Model 이나 Instancing 은 손대지 마시고 OK 버튼을 누르세요.
그리고 프로젝트 전체를 저장한번 하세요. 저는 아래 그림처럼 파일을 저장했습니다.
after_save.jpg

이제 기본적인 프로젝트 생성은 끝났습니다. 조금 막막하시죠?
프로젝트 관리자에서 보면 생성된 파일들 중에서 밑에서 두번째 있는 파일이 저희가 만들 BHO 모듈입니다. 저는 이름을 "popup_blocker_bho.cpp" 라고 저장했습니다.

프로젝트에 COM Object를 추가할때 이름을 "PopupBlocker"라고 줬었는데 기억나세요?
"popup_blocker_bho.cpp" 파일을 열어보시면 TPopupBlockerImpl 이라는 클래스가 정의되어 있을 겁니다. 여러분이 다른 이름을 줬다면 그 이름 끝에 "~Impl" 형태일 겁니다. 파일을 찾으셨다면 한번 열어보세요. 아래 그림과 같나요?

bho_source.jpg

지금까지는 COM 서버를 만드는 과정이었습니다. 여러분이 "COM 서버를 만들어주세요" 라는 개발 의뢰를 받으셨다면 지금처럼

1.ActiveX Library 프로젝트를 만들고
2."File > New > Other" 매뉴에서 "COM Object" 를 추가합니다.
3.COM 객체 생성 화면에 개발을 의뢰한 쪽에서 원하는 COM 서버의 이름을 입력합니다.
4.프로젝트를 저장합니다.
5.COM 서버 소스 파일을 열어서 작업합니다.

이렇게 작업하시면 됩니다.
그럼, 이제부터는 MSDN에서 친절하게 설명해 놓은 BHO를 만드는 법 자료를 읽고 BHO 모듈을 만드는 방법을 알아볼까요?

2.IObjectWithSite 인터페이스 구현

MSDN 에 보면 BHO 를 만들려면 IObjectWithSite 라는 인터페이스를 구현해야한다고 합니다. 작업은 아주 간단합니다.
우선 IObjectWithSite 라는 인터페이스가 어떤 헤더에 정의되어 있는지 알아야하니까, MSDN 또는 C++빌더의 도움말에서 찾아봅시다.
MSDN 에는 다음의 주소에 IObjectWithSite 인터페이스에 대한 정보가 있습니다.
http://msdn.microsoft.com/en-us/library/aa768220.aspx

여기 보면 'ocidl.h' 라는 헤더 파일에 정의되어 있다고 나오네요.
그럼 헤더 파일도 알았으니 이제부터 코딩을 해 봅시다. "popup_blocker_bho.h" 파일을 열어서 "ocidl.h" 파일을 인크루드하세요. 아래처럼요.

01.
02.// 1.2
03.// Unit6.h : Declaration of the TPopupBlockerImpl
04.#ifndef popup_blocker_bhoH
05.#define popup_blocker_bhoH
06.#define _ATL_APARTMENT_THREADED
07.#include "popup_blocker_TLB.h"
08.//추가된코드
09.#include "ocidl.h"

그리고 저희가 만든 BHO 가 IObjectWithSite 인터페이스를 구현하도록 합시다. TPopupBlockerImpl 클래스의 선언부에 아래와 같이 IObjectWithSite 를 상속하도록 하세요.

1.
2.class ATL_NO_VTABLE TPopupBlockerImpl :
3. public CComObjectRootEx<CComSingleThreadModel>,
4. public CComCoClass<TPopupBlockerImpl, &CLSID_PopupBlocker>,
5. public IPopupBlocker,
6. //추가된코드
7. public IObjectWithSite

그리고나서 클래스가 정의된 아래쪽을 보면 아래와 같은 부분이있습니다. 여기에 TPopupBlockerImpl 클래스가 IObjectWithSite 인터페이스를 구현하고 있다는 것을 표시해야합니다. 아래와 같이요.

1.
2.BEGIN_COM_MAP(TPopupBlockerImpl)
3. COM_INTERFACE_ENTRY(IPopupBlocker)
4. //추가된코드
5. COM_INTERFACE_ENTRY(IObjectWithSite)
6.END_COM_MAP()

아시는 분들도 있겠지만, 인터페이스는 순수 추상 클래스와 같습니다. 즉, 메서드들이 정의만되어 있고 구현되어 있지 않습니다. 그래서 순수 추상 클래스를 상속 받으면 상속한 클래스는 부모에 정의된 순수 가상 메서드들을 모두 구현해야합니다. 그렇지 않으면 클래스의 인스턴스를 생성할 수 없게됩니다.
저희도 BHO를 만들기 위해서 MSDN에서 말해준 IObjectWithSite 라는 인터페이스를 구현하려고 하고 있습니다. 그래서 TPopupBlockerImpl 클래스 선언부에 "public IOjectWithSite" 라고 선언 했구요. 이제부터는 TPopupBlockerImpl 클래스가 IObjectWithSite 인터페이스의 메서드들을 구현하도록 선언해야 합니다.

아까 IObjectWithSite 인터페이스가 어떤 헤더 파일에 있었죠?
맞습니다. "ocidl.h" 파일이었습니다. "#include "ocidl.h" 라고 아까 작성했던 부분에 커서를 두고 "Ctrl+Enter" 키를 누르세요.
그럼 아래와 같이 "ocidl.h" 파일이 열립니다.

ocidl_h.jpg

이제 Ctrl+F 를 눌러서 "IObjectWithSite" 라고 입력해서 정의된 부분을 찾아봅시다.
어떤 메서드들이 정의되어 있는지 알아야 구현을 하던지 말던지 할거 아니겠어요?

find_iobjectwithsite.jpg

"F3"키를 11번 누르니까 IObjectWithSite 인터페이스 정의가 나오네요.


iobjectwithsite.jpg
저처럼 찾으셨나요? 정의가 일반 클래스와는 사뭇 다릅니다. 생김새는 별로 신경 안쓰서도 됩니다. 자바나 델파이는 명시적으로 인터페이스를 구현할 수 있도록 되어 있지만 C++ 언어에는 인터페이스라는 용어가 없기 때문에 보통 struct 나 class 로 구현을 합니다. 그래서 다른 언어와는 조금 다른 모습이지만 신경쓰지 않아도 됩니다. 중요한 것은 이 인터페이스에 정의된 메서드들입니다.

IObjectWithSite 인터페이스에서 위의 그럼처럼 메서드 정의 부분만 복사하세요. 그리고 TPopupBlockerImpl 클래스에 붙여넣으세요.
(복사해서 붙여넣는 식의 코딩을 개인적으로 싫어하지만 이런걸 굳이 타이핑칠 필요는 없겠죠?)
그리고나서 복사한 메서드들은 더 이상 순수 가상 메서드가 아니므로 앞에 있는 "virtual" 키워드와 끝에 붙은 " = 0" 을 제거하세요. 그러면 아래와 같이 됩니다.

인터페이스를 여러개 구현하게될 경우, 인터페이스의 메서드만 나열하면 나중에 어떤 메서드가 어떤 인터페이스에 있는 것이지 알아보기 힘들어지는 경우가 많습니다. 그래서 아래와 같이 주석을 넣어주는 것이 좋습니다.

01.
02.// IPopupBlocker
03.public:
04.//추가된코드
05.//IObjectWithSite 인터페이스
06.public:
07. HRESULT STDMETHODCALLTYPE SetSite(
08. /* [in] */ __RPC__in_opt IUnknown *pUnkSite);
09. HRESULT STDMETHODCALLTYPE GetSite(
10. /* [in] */ __RPC__in REFIID riid,
11. /* [iid_is][out] */ __RPC__deref_out_opt void **ppvSite);

그러면 지금까지 작업한 TPopupBlockerImpl 클래스는 아래와 같아집니다. 그리고 주석으로 표시된 부분이 지금까지 추가된 코드들입니다.

01.
02.// 1.2
03.// Unit6.h : Declaration of the TPopupBlockerImpl
04.#ifndef popup_blocker_bhoH
05.#define popup_blocker_bhoH
06.#define _ATL_APARTMENT_THREADED
07.#include "popup_blocker_TLB.h"
08.//추가된코드
09.#include "ocidl.h"
10./////////////////////////////////////////////////////////////////////////////
11.// TPopupBlockerImpl Implements IPopupBlocker, default interface of PopupBlocker
12.// ThreadingModel : tmApartment
13.// Dual Interface : FALSE
14.// Event Support : FALSE
15.// Default ProgID : Project5.PopupBlocker
16.// Description :
17./////////////////////////////////////////////////////////////////////////////
18.class ATL_NO_VTABLE TPopupBlockerImpl :
19. public CComObjectRootEx<CComSingleThreadModel>,
20. public CComCoClass<TPopupBlockerImpl, &CLSID_PopupBlocker>,
21. public IPopupBlocker,
22. //추가된코드
23. public IObjectWithSite
24.{
25.public:
26. TPopupBlockerImpl()
27. {
28. }
29. // Data used when registering Object
30. //
31. DECLARE_THREADING_MODEL(otApartment);
32. DECLARE_PROGID("popup_blocker.PopupBlocker");
33. DECLARE_DESCRIPTION("");
34. // Function invoked to (un)register object
35. //
36. static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
37. {
38. TTypedComServerRegistrarT<TPopupBlockerImpl>
39. regObj(GetObjectCLSID(), GetProgID(), GetDescription());
40. return regObj.UpdateRegistry(bRegister);
41. }
42.
43.DECLARE_GET_CONTROLLING_UNKNOWN()
44.BEGIN_COM_MAP(TPopupBlockerImpl)
45. COM_INTERFACE_ENTRY(IPopupBlocker)
46. //추가된코드
47. COM_INTERFACE_ENTRY(IObjectWithSite)
48.END_COM_MAP()
49.// IPopupBlocker
50.public:
51.//추가된코드
52.//IObjectWithSite 인터페이스
53.public:
54. HRESULT STDMETHODCALLTYPE SetSite(
55. /* [in] */ __RPC__in_opt IUnknown *pUnkSite);
56. HRESULT STDMETHODCALLTYPE GetSite(
57. /* [in] */ __RPC__in REFIID riid,
58. /* [iid_is][out] */ __RPC__deref_out_opt void **ppvSite);
59.};
60.#endif //Unit6H

이제 실제 IObjectWithSite 인터페이스의 두 함수 (SetSite() 와 GetSite())에 대한 최소 코드를 넣어봅시다.

MSDN의 BHO 개발 관련 기사에서 "The IObjectWithSite Interface" 절을 보면 SetSite() 함수는 인터넷 익스플로러에 BHO가 로드되었을때 호스트 환경에 대한 정보를 전달하기 위해서 인터넷 익스플로러에 호출한다고 되어 있습니다. 즉, BHO를 초기화하는 과정에서 호출되는 함수이며 브라우저와 상호작용을 하기 위해서 웹 브라우저 인터페이스를 전달하는 것입니다.

그리고 GetSite() 함수에 대해서는 특별한 언급은 없고 단지, SetSite() 에서 파라메터로 전달받은 pUnkSite 포인터에 대해서 요청받은 인터페이스를 쿼리 해야 한다고 되어 있습니다.

그러면 MSDN에서 하라고 하는대로 해야겠죠?

우선 SetSite()에서 파라메터로 받은 pUnkSite 인터페이스 포인터를 저장할 변수가 필요합니다. 그래서 클래스 상단에 IUnknown 타입의 멤버 변수를 선언하겠습니다. 아래와 같이요.

01.
02.class ATL_NO_VTABLE TPopupBlockerImpl :
03. public CComObjectRootEx<CComSingleThreadModel>,
04. public CComCoClass<TPopupBlockerImpl, &CLSID_PopupBlocker>,
05. public IPopupBlocker,
06. //추가된코드
07. public IObjectWithSite
08.{
09. //추가된 코드
10.private:
11. IUnknown *mSite;
12.public:
13. TPopupBlockerImpl()
14. : mSite(NULL)
15. {
16. }

저는 mSite 라는 IUnknown 인터페이스 포인터 타입으로 선언했습니다. 그리고 클래스의 생성자에서 mSite 변수를 NULL 로 초기화했습니다.

이제 BHO의 cpp 파일("popup_blocker_bho.cpp")을 열어서 IObjectWithSite 인터페이스의 두 함수를 정의하세요.

01.
02. // 1.1
03.// UNIT6 : Implementation of TPopupBlockerImpl (CoClass: PopupBlocker, Interface: IPopupBlocker)
04.#include <vcl.h>
05.#pragma hdrstop
06.#include "popup_blocker_bho.h"
07.
08./////////////////////////////////////////////////////////////////////////////
09.// TPopupBlockerImpl
10.
11.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::SetSite(
12. /* [in] */ __RPC__in_opt IUnknown *pUnkSite)
13.{
14.//웹 브라우저가 전달한 인터페이스 포인터를 저장한다.
15. mSite = pUnkSite;
16.return S_OK;
17.}
18.
19.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::GetSite(
20. /* [in] */ __RPC__in REFIID riid,
21. /* [iid_is][out] */ __RPC__deref_out_opt void **ppvSite)
22.{
23.//브라우저가 요청하는 인터페이스를 질의해서 리턴한다.
24. return mSite->QueryInterface(riid, ppvSite);
25.}

소스를 보시면 별거 없네~ 라고 생각드실 겁니다. 앞에서 말한 그대로 작성했습니다.

여기까지가 IObjectWithSite 인터페이스를 구현하기 위한 작업이었습니다. 이처럼 "COM 서버가 A, B, C 인터페이스를 구현해야 합니다 (또는 지원해야 합니다)." 라는 말을 들으면 지금 처럼

1.A, B, C 인터페이스가 정의된 헤더 파일을 찾아서 인크루드시킨다.
2.COM 서버를 구현하는 클래스가 A, B, C 인터페이스를 상속하도록 선언한다.
3.COM_INTERFACE_ENTRY() 매크로로 A, B, C 인터페이스를 각각 선언한다.
4.각 인터페이스의 메서드들의 정의를 찾아서 COM서버를 구현하는 클래스에 동일하게 선언한다.
5.각 인터페이스의 메서드들을 cpp 파일에 정의한다.

이렇게하면 여러분이 만든 COM서버가 인터페이스를 구현하는 것입니다.

3.BHO 등록하는 코드 작성

이제는 우리가 만든 BHO를 인터넷 익스플로러와 탐색기가 인식하도록 레지스트리에 등록하는 부분을 작성해보겠습니다.

MSDN의 BHO 개발 방법에 관한 기사에서 "Registration of Helper Object" 절을 보면 아래의 레지스트리 위치에 BHO 의 클래스 아이디를 키로 등록해야한다고 나와있습니다.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects

레지스트리 편집기를 실행해서 실제로 우리 컴퓨터에 설치된 BHO에는 어떤 것들이 있는지, 다른 사람들은 어떻게 등록했는지 한번 살펴봅시다. "regedit.exe" 를 실행해서 위의 경로에 있는 키들을 한번 보세요.

저는 아래와 같이 총 네개의 BHO가 설치되어 있네요. 알툴바를 깔았더니 알툴바도 BHO를 설치한다는 것을 알수 있네요. 혹시 시스템을 새로 까셨거나 BHO를 설치하는 프로그램이 하나도 없다면 키가 아예 없을 수도 있습니다.

bho_registry.jpg

등록된 BHO 들을 살펴보면 MSDN에서 말한 것 처럼 값은 없고 단지 BHO의 클래스 아이디만 키로 등록되어 있습니다.

별거 아니죠? 그럼 우리도 같은 곳에 PopupBlocker를 BHO로 한번 등록해봅시다.

어떻게 등록하냐구요?

우리가 만든 BHO 소스를 열면 아래와 같은 부분이 있습니다.

1.
2.static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
3. {
4. TTypedComServerRegistrarT<TPopupBlockerImpl>
5. regObj(GetObjectCLSID(), GetProgID(), GetDescription());
6. return regObj.UpdateRegistry(bRegister);
7. }

눈치 채셨겠지만, 이부분이 우리가 만든 COM 서버를 레지스트리에 등록해주는 부분입니다. bRegister 라는 BOOL 타입의 파라메터를 받고 있는데, 이 값이 true일때는 COM 서버가 등록될때이며, false 일때는 COM 서버가 제거될때입니다.

C++ 빌더에서 자동으로 만든 이 코드는 우리가 만든 COM 서버 모듈을 레지스트리에 등록해주는 것으로 여기서 등록하는 내용은 COM 서버가 동작하기 위한 아주 기본적인 내용들 뿐입니다. 그렇기 때문에 저희처럼 COM 서버이지만 추가적인 등록과정이 필요한 경우에는 이 함수를 수정할 필요가 있습니다.

그럼 이번에는 위에 있는 코드가 실행되면 어떤 내용이 등록되는지 한번 볼까요? 앞에서 살펴봤던 BHO 가 등록되는 레지스트리를 다시 열어서 등록된 BHO들 중에서 아무거나 하나 선택해서 그 키 값을 메모해두세요. 저는 아까 봤던 알툴바의 BHO 를 찾아보겠습니다.

altoolbar_bho_reg.jpg

"HKEY_CLASSES_ROOT\CLSID" 를 찾아보세요. 모든 COM 서버는 여기에 등록됩니다. 알툴바 BHO의 키가 "{7F1A79F9-78D1-4186-9F60-EE0B63DF042A}" 였는데 찾아보니 위의 그림과 같았습니다. 그리고 하위 키들을 보니 "InprocServer32"라는 키가 있고 거기에 기본 값으로 알툴바 BHO 모듈이 있는 경로가 나와있네요.

아까 말씀드렸던 UpdateRegistry() 함수는 위의 그림과 같은 레지스트리 값을 기록하는 역할을 합니다.

MSDN의 BHO 개발 관련 기사를 이미 살펴보셨거나 COM에 대해서 알고 계신다면 BHO가 어떻게 로드되는지 아셨을 겁니다.

인터넷 익스플로러 또는 탐색기가 실행될때 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects" 레지스트리 키 밑에 있는 모든 키를 읽어들인 다음 각 키에 대해서 COM 객체를 생성합니다. 그러면 COM 서브 시스템에서 "HKEY_CLASSES_ROOT\CLSID" 레지스트리 키 밑에서 해당하는 BHO 키를 가진 COM 서버 모듈을 로드한 다음 COM 객체를 생성하는 것입니다.

여기서 COM과 관련된 세부 내용은 굳이 모르셔도 BHO를 개발하는데 큰 무리는 없습니다. 다만, 모르시는 분들이 많아서 간략하게 어떤 식으로 동작하는지를 알려드리려는 것 뿐이니까 너무 부담 가지지 마세요. 그리고 COM 과 관련해서 세부적인 내용을 알고 싶으시면 "Inside COM" 이라는 책을 한번 보세요. "좋은 책 있으면 소개시켜줘!" 게시판에서도 제가 소개한 책인데, 소장할 가치가 있는 책입니다.

이제 우리가 만든 BHO를 한번 등록하는 코드를 작성해봅시다. 그런데, 우리가 만든 BHO의 클래스 아이디는 뭘까요? UpdateRegistry() 함수를 대충 보니 GetObjectCLSID() 라는 함수만 호출하면 클래스 아이디를 얻을 수 있는거 같기는 한데, 그래도 어떤 값이 등록될지 개발자가 알고는 있어야 겠죠?

C++ 빌더에서 "View > Type Library" 메뉴를 실행해보세요. 그러면 아래 그림처럼 우리가 만든 COM 서버에서 생성하는 COM 객체들 목록이 나옵니다. 우리는 하나만 만들었으니 당연히 한개만 보이겠죠?

com_objects.jpg

그런데, 목록을 보니 여러개가 있습니다. 그리고 선택해보면 모두 GUID라는 것을 가지고 있구요. 어떤걸까요?

박스위에 공이 언쳐있는 듯한 모양의 아이콘이 있는 것이 우리가 찾던 것입니다. 이름도 "PopupBlocker" 라고 되어 있고 타입에 "CoClass" 라고 되어 있습니다. 여기에 나와있는 GUID 값이 아까 말했던 UpdateRegistry() 함수에서 GetObjectCLSID() 함수의 리턴값입니다.

그리고 레지스트리에 키를 생성하는 API 다들 아시죠? RegCreateKey() 함수입니다. 함수 원형은 아래와 같습니다.

1.
2.LONG RegCreateKey(
3. HKEY hKey,
4. LPCTSTR lpSubKey,
5. PHKEY phkResult
6.);

hKey 파라메터는 루트키를 말하는데, 우리는 "HKEY_LOCAL_MACHINE" 에 넣어야 합니다.

그리고 lpSubKey 는 루트키 하위의 키를 말하는 것으로 "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects"+BHO의 클래스 아이디를 넣어야 합니다.

마지막 phkResult 는 생성된 키 핸들값으로 성공하면 생성된 키핸들이 리턴됩니다.

그래서 아래와 같이 호출해야 겠죠?

1.
2.RegCreateKey(
3. HKEY_LOCAL_MACHINE,
4. "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects\\{C0EE681E-B786-4B23-A74E-58E02B1591D9}",
5. &key);

그리고 BHO가 삭제될때는 등록했던 레지스트리 키를 삭제해줘야 합니다. 이때는 RegDeleteKey() 를 호출하면 되구요. RegDeleteKey() 함수의 원형은 아래와 같습니다.

1.
2.LONG RegDeleteKey(
3. HKEY hKey,
4. LPCTSTR lpSubKey
5.);

파라메터는 RegCreateKey()와 동일합니다. 우리는 아래와 같이 호출하면 되겠죠?

1.
2.RegDeleteKey(
3. HKEY_LOCAL_MACHINE,
4. "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects\\{C0EE681E-B786-4B23-A74E-58E02B1591D9}"
5.);

그럼 BHO를 등록 및 등록 해제하는 부분을 실제 BHO 모듈의 UpdateRegistry() 함수에 넣어 봅시다.

01.
02.static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
03. {
04. //추가된 코드
05. HRESULT ret;
06. std::string reg_path;
07.
08. TTypedComServerRegistrarT<TPopupBlockerImpl> regObj(GetObjectCLSID(), GetProgID(), GetDescription());
09.
10. //변경된 코드
11. ret = regObj.UpdateRegistry(bRegister);
12.
13. //이하 추가된 코드
14. if (ret != S_OK)
15. return ret;
16.
17. //추가할 레지스트리 경로를 만든다.
18. reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects\\";
19. reg_path.append(Sysutils::GUIDToString(GetObjectCLSID()).c_str());
20.
21. //BHO가 등록될때
22. if (bRegister)
23. {
24. HKEY reg_key(NULL);
25. //BHO를 레지스트리에 등록한다.
26. if (RegCreateKey(HKEY_LOCAL_MACHINE, reg_path.c_str(), &reg_key) != ERROR_SUCCESS)
27. return E_FAIL;
28. }
29. //BHO가 등록해제될때
30. else
31. {
32. //BHO와 관련된 키를 삭제한다.
33. if (RegDeleteKey(HKEY_LOCAL_MACHINE, reg_path.c_str()) != ERROR_SUCCESS)
34. return E_FAIL;
35. }
36.
37. return S_OK;
38. }

소스 보고 조금 놀랐을 수도 있는데, 앞에서 설명한 것과 큰 차이는 없습니다. 다만, 생성할 레지스트리 키의 경로를 STL 의 string 을 이용한 것과, 앞에서는 등록할 키 값을 문자열로 직접 넣었었는데, 위에서는 GetObjectCLSID() 함수를 호출해서 BHO의 키 값을 변수로 처리했다는 것입니다.

그리고 생소할 수 있는데, GUID를 문자열로 변환하는 함수 GUIDToString()을 사용했습니다. 소스를 천천히 살펴보면 크게 어려운 부분은 없습니다.

여기까지 하면 인터넷 익스플로러 및 탐색기에서 인식되는 BHO를 만든 것입니다.

4.인터넷 익스플로러에서만 로드하도록 만들기

MSDN 에서 BHO를 만드는 기사에서 "What Are Browser Helper Objects?" 절을 보면, 쉘 버전 4.71 이상에서는 BHO가 탐색기에도 로드된다고 되어 있습니다. 그래서 그 기사에보면 "Detecting Who's Calling"이라는 절이 있습니다. 즉, BHO가 탐색기에 로드되었는지 아니면 인터넷 익스플로러에 로드되었는지를 감지한다는 내용이죠.

그리고 아래와 같은 예제 소스 코드가 있습니다. 이 코드는 BHO의 DllMain() 의 코드로 BHO가 dll 형태로된 인프로세스 COM 서버이기 때문에 다른 프로그램에 로드되면 DllMain() 이 호출됩니다.

그때, BHO가 로드된 프로세스명을 구해서 탐색기일 경우에는 로드되지 않도록 하는 내용입니다. 소스를 한번 살펴볼까요?

01.
02.if (dwReason == DLL_PROCESS_ATTACH)
03.{
04. TCHAR pszLoader[MAX_PATH];
05. GetModuleFileName(NULL, pszLoader, MAX_PATH);
06. _tcslwr(pszLoader);
07. if (_tcsstr(pszLoader, _T("explorer.exe")))
08. return FALSE;
09.}

첫줄을 보면 dwReason 이 DLL_PROCESS_ATTACH 일 경우, 즉, DLL이 프로세스에 로드될 경우에 다음 코드가 실행되도록 되어 있습니다.

그 다음에 보면 GetModuleFileName() 을 호출해서 현재 프로세스의 실행 파일명을 구합니다. 그리고 프로세스명이 "explorer.exe" 인지 확인해서 맞다면 DllMain() 의 리턴 값을 FALSE로 리턴해서 DLL 이 해당 프로세스에 로드되지 않도록 하고 있습니다.

그리고 MSDN 기사에서 주의 사항이 언급되어 있는데, 만약 현재 로드된 프로세스의 파일명이 "iexplorer.exe" 즉, 인터넷 익스플로러인지 비교해서 맞을 경우에만 로드되도록 한다면 BHO를 등록하지 못할 수 있다는 것입니다.

BHO를 등록할때 "regsvr32.exe" 를 이용하는데, "regsvr32.exe" 도 BHO를 로드해서 BHO DLL 파일의 DllRegisterServer() 함수를 단순히 호출하는 것이기 때문에, 앞에서 말한 것처럼 처리하면 BHO가 로드되지 않아서 BHO를 등록하지 못하게되는 것입니다.

(참고로, 보통 엑티브엑스나 COM 서버를 등록할때 어떤 마술이 있는 것으로 아시는 분들이 있는데, 개발툴에서 등록하는 코드를 자동으로 만들고 DllRegisterServer(), DllUnRegisterServer() 함수를 공개하는 것 뿐입니다. 이 함수명은 COM 스팩에 명시된 것들입니다. 그래서 어떤 dll 이 COM 서버인지 확인하려면 단순히 dll 파일이 위의 함수를 공개하고 있는지만 봐도 알수 있습니다.

눈치채셨겠지만, COM 서버는 스스로 등록하는 구조입니다. 시스템에서 특별히 해주는 것은 없고 개발자가 스스로 COM 스팩에 정의된 위치에 레지스트리 키와 값들을 만들어 주는 것입니다. 그런데, 이런 작업이 귀찮기 때문에 개발툴에서 라이브러리 형태로 자동화한 것 뿐이죠.

환상이 좀 깨셨나요? ㅎㅎ 이미 알고 계셨다면 죄송...ㅎㅎ)

어째튼 MSDN에 나와있는 대로 해야합니다.

그럼 우리가 만든 BHO의 DllMain() 은 어디 있을 까요? 한번 찾아봅시다. 저는 "popup_blocker.cpp"에 있습니다. 파일명이 저와 다를 수 있으니 cpp 파일들을 한번씩 열어보세요. 그럼 저처럼 "DllEntryPoint()" 라는 함수가 있는 파일이 있을 겁니다.

dllmain.jpg

찾으셨나요? 여기가 아까 말씀드린 코드를 삽입할 위치입니다.

기존에 있는 코드는 그대로 두고, 앞에서 작성한 코드를 그대로 넣으면 됩니다. 저처럼요.

01.
02.int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
03.{
04. if (reason == DLL_PROCESS_ATTACH)
05. {
06. _Module.Init(ObjectMap, hinst);
07. DisableThreadLibraryCalls(hinst);
08.
09. //추가된 코드
10. TCHAR pszLoader[MAX_PATH];
11.
12. GetModuleFileName(NULL, pszLoader, MAX_PATH);
13. _tcslwr(pszLoader);
14.
15. if (_tcsstr(pszLoader, _T("explorer.exe")))
16. return FALSE;
17. }
18.
19. return TRUE;
20.}

이렇게하면 BHO가 인터넷 익스플로러에서만 로드됩니다.

5.결과

지금까지 인터넷 익스플로러에서만 로드되는 BHO 모듈을 만들었습니다. 이제는 지금까지 만든 BHO가 실제로 동작하는지 확인해 보도록 하겠습니다.

지금까지의 소스를 빌드하신 후, 명령창을 하나 띄우세요. 저는 편의를 위해서 "D:\tmp" 라는 폴더를 하나 만들어서 거기에 빌드된 "popup_blocker.dll" 파일을 복사했습니다.

자 이제 BHO를 등록해보겠습니다. 명령창에 "regsvr32 BHO 파일 경로" 를 치세요. 저는 BHO가 "D:\tmp"에 있어서 "regsvr32 d:\tmp\popup_blocker.dll" 이라고 입력했습니다.

regsvr32_error.jpg

윈도우 XP를 쓰시는 분들은 정상적으로 되셨을 텐데, 저 처럼 비스타를 쓰시는 분들은 위의 그림과 같은 메시지가 나올 겁니다.

"popup_blocker.dll" 을 로드했지만 DllRegisterServer 호출에 실패했다고 하네요. 왜그런걸까요?

이유는 BHO 가 등록될때 HKEY_CLASSES_ROOTHKEY_LOCAL_MACHINE 을 접근하는데, 이는 관리자 권한이 없이는 접근이 안되기 때문입니다. 그럼 관리자 권한으로 실행하면 되겠죠? 아래와 같이 입력해보세요.

runas /user:administrator "regsvr32 d:\tmp\popup_blocker.dll"

어떠세요? 저처럼 정상적으로 등록되었나요? 이 명령은 뒤에 따옴표로 되어 있는 명령을 "/user" 에 지정된 계정 권한으로 실행하는 명령입니다. 알아두시면 요긴하게 사용하실 날이 있습니다.

regsvr32_success.jpg

이때, 관리자 암호를 묻는데 암호를 입력하셔야 저 처럼 성공합니다.

그럼 이제는 BHO가 레지스트리에 올바른 값을 입력했는지 확인해볼까요? 레지스트리 편집기를 실행해보세요. 앞전에 제가 알려드린 BHO 등록 경로 기억나시나요? 여기를 한번 열어보세요.

"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects"

bho_registered.jpg

저는 이렇게 당당히 등록되어 있네요. 그럼 이제 인터넷 익스플로러를 하나 띄워서 BHO가 로드되는지 살펴봅시다. 어때요? BHO가 로드되었나요?

BHO가 로드됬는지 어쨌는지 알수가 없군요. 메시지를 하나 넣어서 로드될때 띄워봅시다. 그래야 이넘이 로드되었는지 알수 있죠.

SetSite() 함수에 간단한 메시지를 출력하는 코드를 저처럼 넣어보세요.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::SetSite(
03. /* [in] */ __RPC__in_opt IUnknown *pUnkSite)
04.{
05. //추가된 메시지
06. MessageBox(NULL, "저 여기 있어요!!", "www.soft005.com", MB_OK);
07.
08. //웹 브라우저가 전달한 인터페이스 포인터를 저장한다.
09. mSite = pUnkSite;
10. return S_OK;
11.}

이후 과정은 아시죠? 빌드하고 등록하고... 그리고나서 웹 브라우저를 다시 띄워보세요. 그럼 아래처럼 우리가 추가한 메시지가 출력됩니다.

참, 등록은 하지 않고 파일만 교체하면 됩니다. 아까 살펴봤듯이 등록이라는게 단순히 레지스트리에 BHO 가 어디 있는지를 알려주는 것 말고 하는일이 없기 때문입니다.

그런데, 복사가 안되시면 현재 열러있는 익스플로러를 닫고 다시해 보세요. 익스플로러에 이미 로드가 되어있어서 교체가 않될 수 있습니다.

bho_msg.jpg

어떻습니까? 저처럼 메시지가 뜨나요?

여기까지가 아무런 역할을 하지 않는 BHO를 만들고 등록하는 과정이었습니다.

끝으로 다음 과정을 가기 전에 등록된 BHO를 제거해주세요. 아래 명령을 명령창에 입력하신 후에 관리자 암호 넣으시면 BHO가 제거되니다. 그리고 열려있는 브라우저 창을 모두 닫으세요.

runas /user:administrator "regsvr32 /u d:\tmp\pupup_blocker.dll"

참, 브라우저 창 닫으실때 아까 삽입한 메시지가 한번 더뜨죠? 그건 BHO가 파괴되기전에 브라우저가 SetSite()를 한번더 호출하기 때문입니다. 이때는 pUnkSite 가 NULL로 전달됩니다.

Part 3. 팝업을 차단하는 BHO 만들기

이제서야 우리가 만들려고하는 팝업을 차단시키는 BHO 모듈을 만들때가 왔습니다. Part2 에서 만들었던 BHO를 이용해서 이번에는 실제 웹 브라우저의 이벤트를 받아서 새창이 열리는 것을 막는 것을 해보도록 하겠습니다.

Part3. 팝업을 차단하는 BHO의 예제 소스는 여기(3rd_popup_blocker.zip )있습니다.

1.기본 구조 만들기

Part1 에서는 TWebBrowser 컨트롤을 프로그램에 넣고 OnNewWindow2 이벤트 핸들러에서 Cancel 변수를 true로 설정해서 팝업이 뜨는 것을 차단했었습니다. 기억나시나요?

처음에는 브라우저가 우리 프로그램 안에 들어와 있었는데, 이번에는 우리가 브라우저 안에 들어와 있는 격입니다.

MSDN의 BHO 개발 기사에서 "Get in Touch with WebBrowser" 절을 보면 SetSite에서 pUnkSite 인터페이스를 이용해서 IWebBrowser2 인터페이스를 얻을 수 있다고 되어 있습니다. 이 인터페이스가 TWebBrowser 컨트롤과 같은 역할을 하는 인터페이스 입니다.

여기서 잠깐 COM 에서 정의하고 있는 이벤트 매커니즘에 대해서 간략하게 살펴보는 시간을 갖도록 하겠습니다.

보통 COM 객체를 사용할때는 CoCreateInstance() 함수를 이용해서 COM 객체를 생성해서, 해당 인터페이스의 메서드를 호출해서 어떤 기능을 사용하곤 합니다. 호출 방향이 단방향이죠. 그런데, 가끔씩은 COM 객체가 어떤 이벤트를 외부에 알려줄 필요도 있습니다. 그래서 MS 에서 이런 경우를 위해서 IConnectionPointContainer 와 IConnectionPoint 라는 두개의 인터페이스를 정의해 놓았습니다.

이벤트를 발생시키는 쪽에서 구현하는 인터페이스들로, 이벤트를 받고싶은 쪽에서는 IConnectionPointContainer 인터페이스를 구해서 받고 싶은 이벤트 인터페이스 아이디를 이용해서 IConnectionPoint 인터페이스를 얻고, IConnectionPoint의 Advise() 함수를 호출하면 COM 객체로부터 이벤트를 받을 수 있게 됩니다.

Advise() 함수를 호출하면 쿠키라는 정수값을 리턴 받는데, 나중에 이벤트를 더 이상 받고 싶지 않을때 UnAdvise() 함수에 이 값을 파라메터로 전달하면 이벤트가 더 이상 전달되지 않게됩니다.

그리고 COM에서는 이벤트도 모아서 하나의 인터페이스로 정의합니다. 그래서 IConnectionPointContainer 인터페이스가 있는 것입니다. IConnectionPointContainer 로부터 얻은 IConnectionPoint 인터페이스는 하나의 이벤트 인터페이스와의 연결을 의미합니다.

그럼 웹 브라우저의 이벤트는 어떤 형태로 정의되어 있는지 MSDN에서 한번 찾아봅시다. MSDN에서 "DWebBrowserEvents2" 라고 검색해보세요. 그러면 아래와 같은 자료가 나옵니다.

http://msdn.microsoft.com/en-us/library/aa768283(VS.85).aspx

dwebbrowserevents.jpg

자세히 보면 Part 1 에서 만들었던 프로그램에서 봤던 이벤트가 나와있습니다. COM 인터페이스들이 보통 첫글자를 "I" 로 정의하는데 비해서 이벤트와 관련된 인터페이스들은 "D"를 사용합니다.

우리가 받아야하는 이벤트가 바로 DWebBrowserEvents2 인터페이스에 정의되어 있습니다.

설명해야할 것들이 더 있지만 일단 하나씩 해나가도록 하겠습니다.

우선은, SetSite() 함수에서 파라메터로 넘어온 pUnkSite에서 IWebBrowser2 인터페이스를 얻어봅시다. pUnkSite에서 바로 IConnectionPointContainer 인터페이스를 얻어도 되지만, IWebBrowser2 인터페이스가 나중에 필요하기 때문에 IWeBrowser2 인터페이스를 먼저 구해서 저장해 두고 나서 IConnectionPointContainer 인터페이스를 구하도록 하겠습니다.

IWebBrowser2 인터페이스는 "Exdisp.h" 헤더에 정의되어 있으니 이 파일을 먼저 인크루드 하시고, IWebBrowser2 인터페이스를 저장할 멤버 변수를 하나 만드세요.

01.
02.// 1.2
03.// Unit6.h : Declaration of the TPopupBlockerImpl
04.
05.#ifndef popup_blocker_bhoH
06.#define popup_blocker_bhoH
07.
08.#define _ATL_APARTMENT_THREADED
09.
10.#include "popup_blocker_TLB.h"
11.#include "ocidl.h"
12.//추가된 코드
13.#include "exdisp.h"
14.#include <string>
15.
16./////////////////////////////////////////////////////////////////////////////
17.// TPopupBlockerImpl Implements IPopupBlocker, default interface of PopupBlocker
18.// ThreadingModel : tmApartment
19.// Dual Interface : FALSE
20.// Event Support : FALSE
21.// Default ProgID : Project5.PopupBlocker
22.// Description :
23./////////////////////////////////////////////////////////////////////////////
24.class ATL_NO_VTABLE TPopupBlockerImpl :
25. public CComObjectRootEx<CComSingleThreadModel>,
26. public CComCoClass<TPopupBlockerImpl, &CLSID_PopupBlocker>,
27. public IPopupBlocker,
28. //추가된코드
29. public IObjectWithSite
30.{
31.private:
32. IUnknown *mSite;
33. //추가된 코드
34. IWebBrowser *mWebBrowser;
35.public:
36. TPopupBlockerImpl()
37. : mSite(NULL),
38. //추가된 코드
39. mWebBrowser(NULL)
40. {
41. }

이제는 SetSite() 에서 파라메터로 넘어온 pUnkSite 에서 IWebBrowser2 인터페이스를 구해서 mWebBrowser 변수에 저장하는 코드를 작성합시다.

popup_blocker_bho.cpp 파일을 열어서 SetSite() 함수를 아래와 같이 수정하세요.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::SetSite(
03. /* [in] */ __RPC__in_opt IUnknown *pUnkSite)
04.{
05. //웹 브라우저가 전달한 인터페이스 포인터를 저장한다.
06. mSite = pUnkSite;
07.
08. //추가된 코드
09. if (mSite == NULL)
10. return E_INVALIDARG;
11.
12. //추가된 코드
13. if (mSite->QueryInterface(IID_IWebBrowser2, (void**)&mWebBrowser) != S_OK)
14. return E_FAIL;
15.
16. return S_OK;
17.}

그럼 이제는 아까 말했던 대로 IConnectionPointContainer 인터페이스로부터 DWebBrowserEvents2 와 연결할 수 있는 IConnectionPoint 인터페이스를 구해서 이벤트에 연결하는 부분을 작성해 보겠습니다.

연결하는 코드를 작성하기 전에 MSDN에서 웹 브라우저의 이벤트를 받는 방법에 관한 기사를 잠깐 살펴봅시다.

이 기사를 보면, IConnectionPoint 에 IDispatch 인터페이스를 넘기라고 되어있습니다. 그리고 IConnectionPointContainer 의 FindConnectionPoint() 메서드를 호출해서 IConnectionPoint 를 얻으라고 합니다. 이때 연결할 이벤트의 인터페이스 아이디로 DIID_DWebBrowserEvents2 를 주면 된다고도 써 있습니다.

그런데, IConnectionPoint 에 IDispatch 인터페이스를 넘기려면 우리가 만든 클래스에서 이 인터페이스를 구현해야합니다. 그런데 C++ 빌더에서 자동으로 만든 우리의 TPopupBlockerImpl 클래스는 이미 IDispatch 인터페이스를 구현하고 있습니다.

그래서 우리는 이벤트를 받기 위해서 IDispatch 인터페이스를 구현하는 새로운 클래스를 하나 정의할 필요가 있습니다. C++ 빌더의 "File > New > Other" 매뉴를 눌러서 Unit 을 프로젝트에 하나 추가하세요.

new_unit.jpg

그리고 적당한 파일명을 줘서 저장하세요. 저는 "core.cpp"로 저장했습니다. 이 파일에 TPopupBlocker 라는 클래스를 만드세요(다른 이름을 쓰셔도 됩니다. 이름에 의미는 없습니다).

01.
02.//---------------------------------------------------------------------------
03.
04.#ifndef coreH
05.#define coreH
06.class TPopupBlocker
07.{
08. public :
09. TPopupBlocker(void);
10. virtual ~TPopupBlocker(void);
11.};
12.
13.#endif

이제 이 클래스가 IDispatch 인터페이스를 구현하도록 할건데, 먼저 MSDN에서 IDispatch 인터페이스가 어디에 정의되어 있고 어떤 메서드들을 가지고 있는지 잠깐 살펴봅시다. 아래의 주소에 가면 IDispatch 인터페이스에 대한 내용이 있습니다.

http://msdn.microsoft.com/en-us/library/ms221608.aspx

idispatch.jpg

내용을 살펴보니 끝부분에 "oleauto.h" 파일에 IDispatch 인터페이스가 정의되어 있다고 하네요. 그리고 본문을 보면 IDispatch 인터페이스는 IUnknown 인터페이스를 상속했다는 내용이 있습니다. 그래서 IDispatch 뿐만 아니라 IUnknown 인터페이스의 메서드들도 TPopupBlocker 클래스에서 구현해줘야 합니다. IDispatch 와 IUnknown 인터페이스 정의 찾는 법은 앞에서 다뤘으니 여기서는 바로 소스에 넣도록 하겠습니다.

01.
02.//---------------------------------------------------------------------------
03.#ifndef coreH
04.#define coreH
05.
06.//추가된 코드
07.#include <windows.h>
08.#include <oleauto.h>
09.
10.class TPopupBlocker
11. //추가된 코드
12. : public IDispatch
13.{
14. //IUnknown 인터페이스
15. public :
16. HRESULT __stdcall QueryInterface(
17. const IID& iid,
18. void **ppvObject);
19.
20. ULONG __stdcall AddRef(void);
21.
22. ULONG __stdcall Release(void);
23.
24. //IDispatch 인터페이스
25. public:
26. HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
27. /* [out] */ __RPC__out UINT *pctinfo);
28.
29. HRESULT STDMETHODCALLTYPE GetTypeInfo(
30. /* [in] */ UINT iTInfo,
31. /* [in] */ LCID lcid,
32. /* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo);
33.
34. HRESULT STDMETHODCALLTYPE GetIDsOfNames(
35. /* [in] */ __RPC__in REFIID riid,
36. /* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
37. /* [range][in] */ UINT cNames,
38. /* [in] */ LCID lcid,
39. /* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId);
40.
41. /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
42. /* [in] */ DISPID dispIdMember,
43. /* [in] */ REFIID riid,
44. /* [in] */ LCID lcid,
45. /* [in] */ WORD wFlags,
46. /* [out][in] */ DISPPARAMS *pDispParams,
47. /* [out] */ VARIANT *pVarResult,
48. /* [out] */ EXCEPINFO *pExcepInfo,
49. /* [out] */ UINT *puArgErr);
50.
51. public :
52. TPopupBlocker(void);
53. virtual ~TPopupBlocker(void);
54.};
55.
56.#endif

갑자기 복잡한 함수들이 많아져서 당황하셨죠? 알고보면 별거 아니니 너무 겁먹지 마세요. 여기서 중요한 함수는 IUnknown 인터페이스의 QueryInterface() 함수하고 IDispatch 인터페이스의 Invoke() 함수입니다.

어째튼 필요하지 않아도 인터페이스에 정의된 메서드들이니 구현은 해야합니다. "core.cpp" 파일을 열어서 아래와 같이 각 메서드들을 정의하세요.

01.
02.//---------------------------------------------------------------------------
03.
04.#pragma hdrstop
05.#include "core.h"
06.//---------------------------------------------------------------------------
07.
08.#pragma package(smart_init)
09.TPopupBlocker::TPopupBlocker(void)
10.{
11.}
12.
13.TPopupBlocker::~TPopupBlocker(void)
14.{
15.}
16.
17.HRESULT __stdcall TPopupBlocker::QueryInterface(
18. const IID& iid,
19. void **ppvObject)
20.{
21.}
22.
23.ULONG __stdcall TPopupBlocker::AddRef(void)
24.{
25.}
26.
27.ULONG __stdcall TPopupBlocker::Release(void)
28.{
29.}
30.
31.HRESULT STDMETHODCALLTYPE TPopupBlocker::GetTypeInfoCount(
32. UINT *pctinfo)
33.{
34.}
35.
36.HRESULT STDMETHODCALLTYPE TPopupBlocker::GetTypeInfo(
37. UINT iTInfo,
38. LCID lcid,
39. ITypeInfo **ppTInfo)
40.{
41.}
42.
43.HRESULT STDMETHODCALLTYPE TPopupBlocker::GetIDsOfNames(
44. REFIID riid,
45. LPOLESTR *rgszNames,
46. UINT cNames,
47. LCID lcid,
48. DISPID *rgDispId)
49.{
50.}
51.
52.HRESULT STDMETHODCALLTYPE TPopupBlocker::Invoke(
53. DISPID dispIdMember,
54. REFIID riid,
55. LCID lcid,
56. WORD wFlags,
57. DISPPARAMS *pDispParams,
58. VARIANT *pVarResult,
59. EXCEPINFO *pExcepInfo,
60. UINT *puArgErr)
61.{
62.}

여기까지 하면, IDispatch 인터페이스를 구현하는 클래스가 만들어졌습니다.

아까 제가 중요하다고 말한 두개의 메서드 기억나시나요? QueryInterface() 와 Invoke() 였습니다. 이제 그 두 함수에 필요한 코딩을 해볼때 입니다. 우선 QueryInterface()가 호출되었을때 IUnknown 이나 IDispatch 인터페이스를 요구한다면 해당 인터페이스를 리턴하는 코드를 넣도록 하겠습니다.

01.
02.HRESULT __stdcall TPopupBlocker::QueryInterface(
03. const IID& iid,
04. void **ppvObject)
05.{
06. if (ppvObject == NULL)
07. return E_FAIL;
08.
09. if ((iid == IID_IUnknown) || (iid == IID_IDispatch))
10. {
11. *ppvObject = this;
12. return S_OK;
13. }
14.
15. //IUnknown 과 IDispatch 인터페이스 이외에는 구현하고 있지 않다는 뜻입니다.
16. return E_NOTIMPL;
17.}

앞에서도 언급한 적이 있는데, IUnknown 인터페이스는 모든 COM 관련 인터페이스의 루트 인터페이스입니다. 이 인터페이스에는 3개의 함수가 있는데, AddRef(), Release(), QueryInterface() 입니다. AddRef() 와 Release() 는 참조 카운트와 관련된 함수입니다.

참조 카운트가 뭐냐면, 말그대로 인터페이스를 몇명이나 참조(사용)하고 있는지를 나타내는 수입니다. 예를들어서 A, B, C 클래스가 사용하고 있다면 참조 카운트는 3이됩니다. 즉 3 군데서 인터페이스를 사용하고 있는 것이죠.

그래서 IUnknown 인터페이스를 구현하려면 DWORD 타입의 변수를 하나 만들어서 AddRef() 또는 QueryInterface()가 호출될때 1을 증가시키고, Release() 가 호출될때 1을 감소시켜야 합니다. 그리고 Release() 에서 감소된 참조 카운트가 0이면 객체가 스스로 파괴되어야 합니다.

이 내용은 COM 스펙에 정의된 내용입니다.

그럼 참조 카운트를 왜 사용할까요?

파이썬 같은 스크립트 언어에서도 참조 카운트를 사용하는데, 목적은 객체가 파괴될 시점을 알고, 공유되는 객체가 임의로 파괴되지 못하게 막는데 그 목적이 있습니다. 즉, A 라는 곳에서 인터페이스를 사용하고 있는데, B가 자신은 사용이 끝났다고 인터페이스를 구현하고 있는 객체를 파괴시켜버리면 A는 그 인터페이스를 사용하려는 순간 메모리 접근 오류가 발생할 수 있습니다.

그래서 이런 것을 방지하기 위해서 COM 인터페이스를 구현하는 클래스에서 스스로 참조 카운트를 관리해야하며, COM 인터페이스를 사용하는 쪽에서는 QueryInterface() 함수를 통해서 얻은 인터페이스의 사용이 끝난 경우에는 반드시 Release() 를 호출해야 합니다. 그리고 QueryInterface() 를 이용하지 않더라도 다른 클래스로 인터페이스를 전달하는 등과 같이 인터페이스를 참조하는 곳이 늘어나면 반드시 AddRef() 를 호출해서 참조 카운트를 증가시켜줘야 합니다.

그래야 객체가 임의로 파괴되지 않습니다. 그리고 참조 카운트를 증가나 감소시킬때는 멀티 쓰래드에서 동작할 수도 있기 때문에 InterlockedIncrement()InterlockedDecrement() API를 사용해야 안전합니다.

참고로, 델파이에서는 개발자가 이런 참조 카운트에 대한 것을 신경쓰지 않아도 되도록 내부적으로 AddRef() 와 Release() 를 호출하도록 되어 있습니다. 그리고 델파이에는 "as" 라는 키워드가 있는데, 내부적으로는 QueryInterface()를 호출하는 것입니다.

어째튼 델파이가 편한점이 있지만, 다르게 보면 개발자가 지식이 점점 옅어지게되서 간단한 문제도 해결을 못하게되는 문제가 있습니다.

그런데, TPopupBlocker 클래스에서는 참조 카운트를 따로 관리하지 않아도 크게 무리가 없어서 뺐습니다.

IUnknown 인터페이스에 대해서는 이 정도로 하고, 이제 IDispatch 인터페이스에 대해서 잠깐 살펴봅시다.

IDispatch 는 보통 "디스패치 인터페이스" 라고 부르는데, 어떤 COM 객체가 또는 인터페이스가 "디스패치를 지원한다"는 것은 그 COM 객체가 구현하고 있는 인터페이스들을 잘 몰라도 호출할 수 있다는 뜻이 됩니다.

예를 들면, 자바스크립트에서 엑티브엑스 객체의 속성이나 함수를 접근하는 것을 말합니다. 또는 자바 스크립트에서 DOM 객체들을 접근하는 것도 같습니다.

자바스크립트는 어떻게 대한 민국 서울에서 남몰래 내가 만든 엑티브엑스에 만든 함수를 어떻게 알고 호출할 수 있는 걸까요?

이런 궁금증 가져보신적 없나요?

비밀은 바로 디스패치 인터페이스에 있습니다.

자바스크립트에서 엑티브엑스의 Test() 함수를 아래와 같이 호출했다면

1.
2.function JavaFunc()
3.{
4. SomeActiveX.Test("문자열 들어갑니다");
5.}

익스플로러의 자바 스크립트 엔진은 "SomeActiveX" 라는 객체의 디스패치 인터페이스로부터 GetIDsOfNames() 라는 함수를 이용해서 "Test" 에 대한 디스패치 아이디 값을 얻습니다 (참고로, 디스패치로 호출되는 모든 속성과 메서드는 해당 디스패치 인터페이스에서만 유일한 아이디 값을 가지고 있습니다. 물론 그 값은 구현하는 사람이 임의로 부여합니다. 그래서 그 아이디 값으로 디스패치 인터페이스에 있는 속성이나 메서드를 식별합니다. 마치 모든 COM 인터페이스에 유일한 식별자인 GUID 값이 붙는 것과 유사합니다).

그런 다음 GetIDsOfNames() 로 얻은 "Test" 함수의 디스패치 아이디를 가지고 Invoke() 함수를 호출합니다. 그러면 Invoke() 함수가 디스패치 아이디에 해당하는 Test 함수를 실행합니다. 이때 Test() 함수에 파라메터도 Invoke() 함수에 전달됩니다.

이런 방식을 이용하기 때문에 자바 스크립트에서는 엑티브엑스든 COM 서버 객체든지 간에 디스패치 인터페이스로 호출이 가능한 것입니다.

그러면 이런 생각도 들 수 있습니다. "그러면 모든 인터페이스를 디스패치로 구현하지 뭐하러 각각을 인터페이스로 다시 정의합니까?" 라구요. 물론 그렇게 할 수도 있겠지만, 모든 호출을 디스패치 인터페이스를 통해서 호출하게 되면 속도가 느려집니다. 왜냐하면, 단순한 함수를 호출할때도 매번 GetIDsOfName()를 통해서 해당 객체가 지원하는지를 확인해야하고 다시 호출 파라메터를 배열로 만들어서 Invoke() 함수를 호출해야하니까요.

초기에 디스패치 인터페이스는 VB 때문에 만들었다고 합니다. VB에서 COM 객체를 쉽게 쓸 수 있도록 하기 위해서죠.

이렇게 하나의 객체가 지원하는 속성이나 메서드를 접근하기 위해서 어떤 곳에서는 디스패치를 사용하고 싶어하고 다른 곳에서는 속도를 향상시키기 위해서 인터페이스를 통해서 직접 호출하고 싶어합니다. 그래서 나온 거이 "듀얼 인터페이스"입니다. 듀얼 인터페이스는 디스패치 인터페이스를 통해서도 속성이나 메서드를 호출할 수 있고, 인터페이스를 통해서 직접 호출이 가능하도록 지원하는 것을 말합니다.

대신, 구현하는 사람이 죽어나겠죠? ㅎㅎ

이런 얘기들이 COM을 공부하면 항상 나오는 것들이라서 자세히는 몰라도 대충은 알아두는게 좋습니다. 자세한 내용은 앞전에도 추천해드린 "Inside COM" 을 참고하세요.

사설이 조금 길었지만 유용했길 바랍니다. 그리고 위에서 디스패치 아이디를 말했었는데요, 우리가 받고자하는 이벤트가 정의된 DWebBrowserEvents2 인터페이스도 IDispatch 인터페이스에서 상속받은 디스패치 인터페이스 입니다. 그래서 DWebBrowserEvents2에 정의되어 있는 모든 함수들은 각자 디스패치 아이디를 가지고 있습니다. 이 값은 인터넷 익스플로러 만들때 이미 정해 놓았습니다.

그리고 그 값은 "exdispid.h" 파일에 모두 정의되어 있습니다. 이 파일을 "core.h" 파일에서 인크루드하도록 하고 파일을 열어서 한번 봅시다.

exdispid.jpg

이 파일을 자세히 보니 저희가 관심있는 NewWindow2 이벤트의 아이디도 있습니다.

그럼 이제 Invoke() 함수를 작성해봅시다.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlocker::Invoke(
03. DISPID dispIdMember,
04. REFIID riid,
05. LCID lcid,
06. WORD wFlags,
07. DISPPARAMS *pDispParams,
08. VARIANT *pVarResult,
09. EXCEPINFO *pExcepInfo,
10. UINT *puArgErr)
11.{
12. switch (dispIdMember)
13. {
14. case DISPID_NEWWINDOW2 :
15. {
16. MessageBox(NULL, "새창을 여시려구요?", "www.soft005.com", MB_OK);
17. break;
18. }
19. }
20.}

일단, 앞에서 말한 복잡한 COM 관련 내용은 잊어버리시고, 위와 같이하면 새창이 뜰때 메시지 박스가 실행됩니다. 그리고 다른 이벤트에 대한 처리를 더 넣고 싶다면 case 문을 추가하시면 됩니다. 물론, 처리할 이벤트를 "exdispid.h"에서 찾아서 디스패치 아이디를 사용하면 되구요.

여기까지가 팝업을 차단시키기 위한 기본 구조입니다.

2.웹브라우저 이벤트 소스에 연결하기

앞에서는 웹 브라우저에서 새창이 열릴때 메시지 박스를 띄우기 위한 기본 구조를 만들고 COM에 대한 내용을 조금 다뤄봤었습니다.

그런데, 위에서는 웹 브라우저에서 이벤트가 발생했을때 처리하기 위한 코드만 있을 뿐, 웹 브라우저에게 이벤트가 발생했을때 알려달라고 말을 하지 않은 상태로 실제로 이벤트를 받지는 못합니다. 그래서 이번에는 웹 브라우저 이벤트 소스에 연결해서 실제 웹 브라우저 이벤트를 받아서 아까 작성한 메시지 박스가 실행되는 것까지 한번 해보겠습니다.

연결은 어디서 하는 걸까요?

"popup_blocker_bho.cpp" 파일의 SetSite() 에서 하는 것입니다. 내용이 너무 길어져서 잠시 잊어버릴 수도 있는데, BHO가 로드되면 브라우저가 BHO를 초기화하기 위해서 SetSite()를 호출합니다. 그래서 이 메서드가 호출되었을때 웹 브라우저 이벤트 소스에 연결해야 합니다.

연결하는 방법은 앞에서 설명했듯이 IConnectionPointContainer 인터페이스를 얻어서 FindConnectionPoint() 메서드를 호출해서 IConnectionPoint 인터페이스를 구한다음, Advise()를 호출해야합니다.

자 그럼 SetSite()에 한번 작업을 해봅시다.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::SetSite(
03. /* [in] */ __RPC__in_opt IUnknown *pUnkSite)
04.{
05. //웹 브라우저가 전달한 인터페이스 포인터를 저장한다.
06. mSite = pUnkSite;
07.
08. if (mSite == NULL)
09. return E_INVALIDARG;
10.
11. if (mSite->QueryInterface(IID_IWebBrowser2, (void**)&mWebBrowser) != S_OK)
12. return E_FAIL;
13.
14. //추가된 코드
15. IConnectionPointContainer *cpc(NULL);
16. IConnectionPoint *cp(NULL);
17.
18. if (mWebBrowser->QueryInterface(IID_IConnectionPointContainer, (void**)&cpc) != S_OK)
19. return E_FAIL;
20.
21. if (cpc->FindConnectionPoint(DIID_DWebBrowserEvents2, &cp) != S_OK)
22. return E_FAIL;
23.
24. return S_OK;
25.}

이렇게 해서 IConnectionPoint 인터페이스까지 구했습니다. 이제 남은 것은 IConnectionPoint의 Advise()를 호출하는 것 뿐이네요. 어? 그런데 Advice() 함수에 이벤트가 발생했을때 그 이벤트를 처리할 디스패치 인터페이스가 파라메터로 전달되어야 하네요?

뭘 전달해야할까요? 맞습니다. "core.cpp" 파일에 정의한 TPopupBlocker 클래스의 인스턴스를 전달하면됩니다. 그 클래스가 IDispatch 인터페이스를 구현하고 있으니까요.

그럼 TPopupBlocker 클래스의 인스턴스가 필요하니까 "popup_blocker_bho.h" 파일을 열어서 "core.h" 파일을 인크루드시키고 TPopupBlocker 클래스 타입의 맴버 변수를 하나 추가합시다.

01.
02.// 1.2
03.// Unit6.h : Declaration of the TPopupBlockerImpl
04.
05.#ifndef popup_blocker_bhoH
06.#define popup_blocker_bhoH
07.#define _ATL_APARTMENT_THREADED
08.
09.#include "popup_blocker_TLB.h"
10.#include "ocidl.h"
11.#include "exdisp.h"
12.
13.//추가된 코드
14.#include "core.h"
15.#include <string>
16.
17./////////////////////////////////////////////////////////////////////////////
18.// TPopupBlockerImpl Implements IPopupBlocker, default interface of PopupBlocker
19.// ThreadingModel : tmApartment
20.// Dual Interface : FALSE
21.// Event Support : FALSE
22.// Default ProgID : Project5.PopupBlocker
23.// Description :
24./////////////////////////////////////////////////////////////////////////////
25.class ATL_NO_VTABLE TPopupBlockerImpl :
26. public CComObjectRootEx<CComSingleThreadModel>,
27. public CComCoClass<TPopupBlockerImpl, &CLSID_PopupBlocker>,
28. public IPopupBlocker,
29. public IObjectWithSite
30.{
31.private:
32. IUnknown *mSite;
33. IWebBrowser *mWebBrowser;
34. //추가된 코드
35. TPopupBlocker mBlocker;
36. DWORD mCookie;

이제 다시 "popup_blocker_bho.cpp" 파일의 SetSite() 에 IConnectionPoint.Advise() 함수를 호출하는 코드를 넣도록 합시다.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlockerImpl::SetSite(
03. /* [in] */ __RPC__in_opt IUnknown *pUnkSite)
04.{
05. //웹 브라우저가 전달한 인터페이스 포인터를 저장한다.
06. mSite = pUnkSite;
07.
08. if (mSite == NULL)
09. return E_INVALIDARG;
10.
11. if (mSite->QueryInterface(IID_IWebBrowser2, (void**)&mWebBrowser) != S_OK)
12. return E_FAIL;
13.
14. IConnectionPointContainer *cpc(NULL);
15. IConnectionPoint *cp(NULL);
16.
17. if (mWebBrowser->QueryInterface(IID_IConnectionPointContainer, (void**)&cpc) != S_OK)
18. return E_FAIL;
19.
20. if (cpc->FindConnectionPoint(DIID_DWebBrowserEvents2, &cp) != S_OK)
21. return E_FAIL;
22.
23.//추가된 코드
24. if (cp->Advise(&mBlocker, &mCookie) != S_OK)
25. return E_FAIL;
26.
27. return S_OK;
28.}

이제 웹 브라우저 이벤트에 연결하는 것 까지 완료되었습니다. 메시지 박스도 넣어두었으니 빌드해서 테스트 한번 해볼까요? BHO 등록하는 것은 앞에서 다뤘으니 그냥 결과만 보겠습니다.

new_window_notify.jpg

어떤가요? 여러분들도 저와 같이 새창이 뜨기 전에 메시지가 떴나요? ㅎㅎ 성공입니다.

그런데, "확인"버튼한번 눌러보세요. 새창이 뜹니다. 새창이 뜨지 않도록 막는 코드를 넣지 않았습니다. 일단, 여기까지해서 BHO에서 새창이 뜨는 것을 감지하는 기능은 완료되었습니다. 다음 절에서 실제로 팝업 창을 막는 부분을 처리하도록 하겠스니다.

3.팝업창 차단하기

이제 마지막이네요. 앞 절까지는 BHO를 만들어서 새창이 뜨는 것을 감지하는 부분까지 했습니다. 이번에는 실제로 새창이 뜨지 않도록 막는 부분을 처리해보겠습니다.

Part 1에서 팝업창을 차단하기 위해서 작성했떤 프로그램 기억나시나요? 웹 브라우저 컨트롤의 OnNewWindow2 인벤트에서 Cancel 파라메터를 true로 변경하므써 간단히 팝업이 차단되었었습니다. 기억이 안나실수도 있으니 코드를 다시 한번 보죠.

1.void __fastcall TForm1::wbNewWindow2(TObject *ASender, IDispatch *&ppDisp, WordBool &Cancel)
2.{
3. Cancel = true;
4.}

기억 나시나요? 코드가 아주 간단했었죠. 그런데, BHO 방식으로 개발할때는 어떻게 NewWindow2 이벤트를 감지했었나요? IConnectionPoint 의 Advise() 함수를 호출해서 웹 브라우저 이벤트 소스에 연결하고 TPopupBlocker 라는 클래스를 만들고 IDispatch 인터페이스를 구현했습니다. 그러면 웹 브라우저는 새창이 열릴려고 할때 IConnectionPoint.Advise() 함수를 통해서 제공받은 IDispatch 인터페이스의 Invoke() 를 호출합니다. 그러면 IDispatch 인터페이스를 실제로 구현하고 있는 TPopupBlocker 클래스의 Invoke() 함수를 호출됩니다.

그래서 우리는 Invoke() 함수가 호출될때 디스패치 아이디 값으로 NewWindow2 이벤트가 발생했음을 감지했습니다. 기억나시죠?

그러면 그동안 미뤄뒀던 Invoke() 함수에 대해서 자세히 한번 살펴보는 시간을 갖도록 하겠습니다. MSDN에서 IDispatch 인터페이스를 검색해서 Invoke() 함수를 한번 보세요. 아래와 같이 정의되어 있습니다.

01.
02. HRESULT Invoke(
03. DISPID dispIdMember,
04. REFIID riid,
05. LCID lcid,
06. WORD wFlags,
07. DISPPARAMS FAR* pDispParams,
08. VARIANT FAR* pVarResult,
09. EXCEPINFO FAR* pExcepInfo,
10. unsigned int FAR* puArgErr
11.);

이 Invoke() 함수의 파라메터가 모두 8개인데, 하나씩 살펴보겠습니다.

dispIdMember 파라메터는 처음에 봐서 알겁니다. Invoke() 함수를 통해서 호출하려는 속성 또는 메서드의 식별자인 디스패치 아이디를 나타내는 파라메터입니다.

riid 파라메터는 항상 IID_NULL 이 전달되며, lcid 파라메터는 언어를 나타내는 코드입니다.

wFlags 파라메터는 Invoke() 함수로 호출하는 것이 메서드인지, 속성 읽기인지, 속성 쓰기인지를 나타내는 파라메터입니다.

pDispParams 파라메터는 Invoke() 함수로 속성이나 메서드를 호출할때 파라메터 값을 배열 형태로 가지고 있는 자료구조 포인터입니다. 이 자료구조에는 cArgs 라는 멤버가있는데, 전달되는 파라메터의 수를 나타냅니다. 그리고 rgvarg 라는 VARIANT 타입의 포인터는 실제 파라메터를 저장하고 있는 곳에 대한 포인터입니다. 그래서 pDispParams->rgvarg 를 이용해서 발생한 이벤트의 파라메터를 받을 수 있습니다. 그리고 중요한 것이 한가지 있는데, pDispParams->rgvarg 로 전달되는 파라메터는 역순으로 전달됩니다. 그래서 NewWindow2 이벤트의 두번째 파라메터인 Cancel 이 pDispParams->rgvarg[0] 에 전달되고, pDisp 파라메터가 pDispParams->rgvarg[1]에 전달됩니다.

pVarResult 파라메터는 속성 또는 메서드의 실행 결과를 호출자에게 전달하기 위한 파라메터이고, pExcepInfo는 예외가 발생할 경우, 예외에 대한 정보를 호출자에게 전달하기 위한 파라메터입니다.

마지막 ArgErr 는 pDispParams->rgvarg로 전달된 파라메터에 오류가 있을 경우, 오류가 있는 파라메터의 시작 인덱스를 호출자에게 전달하기 위한 파라메터입니다.

이렇게 Invoke() 함수의 파라메터만 봐도 대충 Invoke()를 자세히 알면 많은 것을 할 수 있겠다는 생각이 듭니다. 어째튼 Invoke() 함수를 살펴 봤으니 이번에는 TPopupBlocker 클래스의 Invoke() 함수를 수정해서 팝업을 실제로 차단하는 코드를 작성해 보도록 하겠습니다.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlocker::Invoke(
03. DISPID dispIdMember,
04. REFIID riid,
05. LCID lcid,
06. WORD wFlags,
07. DISPPARAMS *pDispParams,
08. VARIANT *pVarResult,
09. EXCEPINFO *pExcepInfo,
10. UINT *puArgErr)
11.{
12. switch (dispIdMember)
13. {
14. case DISPID_NEWWINDOW2 :
15. {
16. MessageBox(NULL, "새창을 여시려구요?", "www.soft005.com", MB_OK);
17.
18. //추가된 코드
19. //새창이 뜨지 않도록 막습니다.
20. pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE;
21. break;
22. }
23. }
24.}

이제 빌드해서 테스트해보세요. 어떤가요? 메시지가 나왔을때 "확인" 버튼을 누르면 새창이 막히나요?

축하합니다. 여기까지가 팝업창을 막는 BHO를 개발하는 강좌였습니다. 마지막으로 BeforeNavigate2 이벤트를 하나더 받도록 하는 부분과 NewWindow2 이벤트를 처리하는 부분을 좀더 개선하는 것으로 강좌를 마치도록 하겠습니다.

4.코드 개선하기

우선, DWebBrowserEvent2 인터페이스의 BeforeNavigate2 이벤트NewWindow2 이벤트를 찾아보세요. 그리고 두 함수의 정의를 복사해서 TPopupBlocker 클래스에 복사하세요. 저 처럼요.

01.
02. //추가된 코드
03. //웹 브라우저 이벤트
04. public:
05. void BeforeNavigate2(
06. IDispatch *pDisp,
07. VARIANT *Url,
08. VARIANT *Flags,
09. VARIANT *TargetFrameName,
10. VARIANT *PostData,
11. VARIANT *Headers,
12. VARIANT_BOOL *Cancel);
13.
14. void NewWindow2(
15. IDispatch *pDisp,
16. VARIANT_BOOL *Cancel);
17. public :
18. TPopupBlocker(void);
19. virtual ~TPopupBlocker(void);
20.};

그리고 Invoke() 함수를 조금 변경하도록 하겠습니다. 어떻게 할지는 대략 감 잡으셨겠지만, Invoke() 함수에서 BeforeNavigate2() 와 NewWindow2() 함수를 호출하도록 처리할 것입니다. 그러면, Part 1에서 웹 브라우저 컨트롤을 이용해서 만들었을 때 처럼 코딩하기가 편해질 것입니다.

01.
02.HRESULT STDMETHODCALLTYPE TPopupBlocker::Invoke(
03. DISPID dispIdMember,
04. REFIID riid,
05. LCID lcid,
06. WORD wFlags,
07. DISPPARAMS *pDispParams,
08. VARIANT *pVarResult,
09. EXCEPINFO *pExcepInfo,
10. UINT *puArgErr)
11.{
12. //코드를 줄이기 위해서 파라메터가 있는 배열 포인터를 만들었습니다.
13. VARIANTARG *params(pDispParams->rgvarg);
14.
15. switch (dispIdMember)
16. {
17. //새창이 뜰때
18. case DISPID_NEWWINDOW2 :
19. {
20. //변경된 코드
21. this->NewWindow2(
22. params[1].pdispVal,
23. params[0].pboolVal);
24. break;
25. }
26.
27. //주소로 이동하기전
28. case DISPID_BEFORENAVIGATE2 :
29. {
30. //변경된 코드
31. this->BeforeNavigate2(
32. params[6].pdispVal,
33. &params[5],
34. &params[4],
35. &params[3],
36. &params[2],
37. &params[1],
38. params[0].pboolVal);
39. break;
40. }
41. }
42.
43. return S_OK;
44.}

코드를 보면, pDispParams->rgvarg 에 우리가 원하는 파라메터가 있기 때문에 코드를 간략하게 하기 위해서 pDispParams->rgvarg 와 같은 타입의 포인터 변수를 선언하고 params라고 이름 지엇습니다. 초기값은 pDispParams->rgvarg 로 주었구요.

이후에는 params[0], params[1] 같은 식으로 BeforeNavigate2() 와 NewWindow2() 함수에 파라메터를 전달하도록 처리하였습니다. 이제 BeforeNavigate2() 와 NewWindow2() 함수는 Part1 에서 웹 브라우저 컨트롤에 이벤트를 처리했던 것 처럼 코딩이 쉬워졌습니다.

01.
02.void TPopupBlocker::BeforeNavigate2(
03. IDispatch *pDisp,
04. VARIANT *Url,
05. VARIANT *Flags,
06. VARIANT *TargetFrameName,
07. VARIANT *PostData,
08. VARIANT *Headers,
09. VARIANT_BOOL *Cancel)
10.{
11. //이동하려는 주소를 메시지로 출력합니다.
12. MessageBoxW(NULL, Url->pvarVal->bstrVal, L"여기로 이동합니다.", MB_OK);
13.
14. *Cancel = VARIANT_FALSE;
15.}
16.
17.void TPopupBlocker::NewWindow2(
18. IDispatch *pDisp,
19. VARIANT_BOOL *Cancel)
20.{
21. //메시지 박스가 Invoke() 함수에서 여기로 옮겨졌습니다.
22. MessageBox(NULL, "새창을 여시려구요?", "www.soft005.com", MB_OK);
23.
24. //Part1 에서 처럼 코드가 쉬워졌습니다. 새창이 뜨는 것을 막습니다.
25. *Cancel = VARIANT_TRUE;
26.}

이제 빌드해서 테스트해보세요. 그러면 주소 이동할때 이동하려는 주소가 메시지 상자에 뜨고, 새창이 열릴때도 메시지가 뜨면서 새창이 뜨지 않도록 차단됩니다.

그런데, 마지막에 새로 추가한 BeforeNavigate2() 이벤트에서 Url 을 메시지로 출력하는 부분이 조금 이상하게 보이실텐데요, DWebBrowserEvents 에서 발생하는 이벤트들로 전달되는 파라메터에서 Url 은 BSTR 타입으로 전달되는데, DWebBrowserEvents2에서 발생하는 이벤트들로 전달되는 파라메터에서 Url은 참조형 VARIANT로 전달됩니다. 그리고 참조된 VARIANT의 타입이 BSTR 입니다. 그래서 최종 URL을 문자열로 얻으려면 위에서 처럼 Url->pvarVal->bstrVal 로 해야 얻을 수 있습니다.

복잡한건 아니구 VARIANT *Url 로 된 파라메터가 VARIANT에 대한 포인터이기 때문에 Url에서 바로 bstrVal 을 사용하지 않고 pvarVal 로 참조된 VARIANT에 접근한 뒤에 bstrVal을 사용한 것입니다. 즉, 한 단계 더 거친것 뿐입니다.

아래는 BeforeNavigate2 이벤트에서 이동하려는 주소를 메시지 상자로 출력한 스샷입니다. 두번째 BeforeNavigate2 이벤트가 발생했을때 찍은 스샷입니다.

beforenavigate2.jpg

어째튼 긴 강의 읽느라고 수고들 하셨고, 이번 강의를 통해서 COM을 조금이나마 더 이해할 수 있는 계기가 되었으면 하는 바램 뿐입니다.

행복한 하루 되세요.

(P.S. 강의를 쓰면서 결과 스샷 만든다고 하다가 실수로 브라우저를 종료시켜서 강의 끝부분을 다시 썼습니다. -_-; 그래서 빨리 끝내고 싶은 마음에 BeforeNavigate2() 이벤트에서 Url을 문자열 메시지로 출력하는 부분에 대한 처리를 대충하고 성급하게 강의를 맞쳤었습니다. 그런데 아무래도 무책임한 느낌이 들어서 급하게 수정했습니다. 수정 전의 내용을 못보신 분들도 계시겠지만요... 그리고 올린 Part3 예제에서 링크 옵션에서 "Use Dynamic RTL", 패키지 옵션에서 "Build with rumtime package" 옵션을 끄고 다시 빌드해서 올렸습니다. 빌더가 설치되지 않은 곳에서 등록이 안되는걸 확인 못했네요)

 

 

출처 : http://www.soft005.com/zbxe/?mid=dev_article&sort_index=readed_count&order_type=desc&listStyle=list&document_srl=398

 

twitter facebook me2day 요즘
26개(1/2페이지)
PHP & MySQL
번호 제목 글쓴이 조회 날짜
26 [PHP] 파파고 API 관리자 2171 2021.01.29 16:33
25 [PHP] 소녀나라 구인공고 사진 관리자 3664 2018.04.17 12:54
24 [PHP] Zen HTML Selectors 관리자 5958 2014.03.12 17:16
23 [PHP] zen coding~! 젠코딩 Zen HTML Elements 관리자 8184 2014.03.12 17:16
22 [PHP] 에디트 플러스 zen coding CSS 관리자 7628 2014.03.12 17:16
21 [PHP] php + jquery ajax + json 관리자 5706 2014.03.04 15:30
20 [PHP] php 엑셀 파일 생성시 한글깨짐 관리자 8814 2014.02.21 16:31
19 [PHP] 날씨 API, 기상청 날씨 파싱 관리자 10960 2014.01.22 15:32
18 [PHP] 도로명주소 관리자 5802 2014.01.09 11:39
17 [PHP] PHP, AJAX, JSON 리턴 관리자 7457 2013.10.18 17:49
16 [PHP] PHP 변수 초기화 및 조건부 할당 관리자 6536 2013.09.25 00:57
15 [PHP] 킴스큐 썸네일 사이즈 변경 관리자 4873 2013.07.16 14:14
14 [PHP] 간단하게 만드는 캐싱 사진 관리자 5709 2013.05.21 14:11
13 [PHP] 웹문서 긁어와서 저장 관리자 7296 2013.05.10 12:37
>> [PHP] 팝업방지 BHO 사진 관리자 9318 2013.02.28 14:54
11 [PHP] 강제로 파일다운로드 되게 관리자 9520 2013.01.21 16:31
10 [PHP] 리눅스 서버관리 관리자 4207 2013.01.21 15:14
9 [PHP] SI, SM, ERP, EIP, EAI, CMMS, CRM, SCM, GW, KMS 관리자 8845 2013.01.21 15:13
8 [PHP] eclipse 3.7 pdt php 셋팅방법 첨부파일 관리자 6367 2013.01.21 15:12
7 [PHP] PHP 개발 보안가이드 첨부파일 관리자 7265 2013.01.21 15:10
많이 본 글
댓글 많은 글