TWebBrowser 및 IE 갖고놀기
From YYpBD's MediaWiki
01 TWebBrowser 및 IE 갖고놀기
이 글은 제가 IE스파이를 만들면서 MSDN을 뒤져 C소스를 델파이로 바꾸고, 여기저기 기웃거리며 하나씩 주워 모은 자료입니다.
이걸 참고 하셔서 WebBrowser 컨트롤을 쓰는데 조금이라도 도움이 되길 바랍니다.
글을 메모장에서 쓰다보니 보기좋게 꾸지미 못하는점을 쪼금 이해해 주세요.
1. 기본적인 사항
모든 웹브라우저 컴퍼넌트는 IWebBrowser2 라는 인터페이스를 사용합니다.
델파이로만들던 VC로 만들던 VB로 만들던지 암튼간에 무조건 저 인터페이스를 사용합니다.
델파이의 TWebBrowser라는 컴퍼넌트 역시 MS의 웹브라우저 컴포넌트를 포장해서 쓰는관계로 IWebBrowser2 인터페이스를 가지고 있습니다. 바로 OleObject 퍼로퍼티가 그 놈입니다.
IWebBrowser2 외에도 관련된 인터페이스들이 있는데, 계보를 따져보면 아래와 같습니다.
IUnknown
|
IDispatch
|
IWebBrowser
|
IWebBrowserApp
|
IWebBrowser2
이 인터페이스들은 델파이의 컴포넌트 계보와 개념이 비슷합니다.
우리는 IWebBrowser2 의 값을 알고 있기 때문에 필요하다면 그 상위의 인터페이스들을 알아 낼수도 있습니다.
IWebBrowser2로 IWebBrowserApp를 액세스 하기위해서는 이렇게 하면 됩니다.
Web : IWebBrowser2; WebbApp : IWebBrowserApp;
// Web에는 이미 값이 들어가 잇음 if Web.QueryInterface(IID_IWebBrowserApp, WebApp)=S_OK
then 성공적으로 얻어옴 else 실패함
이렇게 하면 됩니다. 하지만 실패할 일이 절대로 없다는 확신이 설때에는 그냥
(Web as IWebBrowserApp)
이렇게만 하면 됩니다.
2. TWebBrowser 컴포넌트 조작상에 알아두면 좋은 팁들
2.1. 컴퍼넌트 초기화 하기
웹브라우저 컴퍼넌트를 초기화하려면 무슨 문서라도 하나 읽어와야 합니다. 보통은 <body></body> 라는 간단한 텍스트를 a.htm 등에 담아 놓고
WebBrowser.Navigate('file://c:/.../a.htm');
이렇게 하면됩니다. 하지만 이건 저같이 무식한 사람이 앞뒤 안가리고 그냥 쓰는 방법이고 좀더 세련되게 흔적없이 하려면
WebBrowser.Navigate('about:blank');
이렇게 하면 됩니다.
2.2. 소스 보기
HTML을 읽은 브라우저에서 소스를 뽑아내기란 아주 복잡한 작업입니다. 느려터진 MSDN의 관련 클래스들을 뒤지다 보면 아마도
Document 클래스안에 DocumentElement 클래스안에 OuterHTML 이라는 프로퍼티를 찾을 수 있습니다. 소스를 메모장에다가 보여주고 싶다면
Memo1.Lines.Text := WebBrowser.OleObject.Document.DocumentElement.OuterHTML;
이렇게 하면됩니다. 이렇게 간단한데 뭐가 복잡하냐구요? 이건 소스는 소스지만 조작된 소스입니다. IE의 HTML파서를 거쳐서 정형화된 소스입니다.
웹브라우저로 로딩을 하면 그놈이 그놈이지만 소스를 보면 분명 원래 소스와는 차이가 있죠. 그렇다면 원래 모양 그대로 뽑아낼려면 어떻게 해야할까요?
뽑아서 스트림으로 저장하는걸 보면 이래요..
function TMainForm.RevealSource(const Document : IHTMLDocument2; Buf : TStream) : Boolean; var WebS : IPersistStreamInit; begin Result := False; if SUCCEEDED( Document.QueryInterface(IID_IPersistStreamInit, WebS) ) then
if SUCCEEDED( WebS.Save(TStreamAdapter.Create(Buf), True) ) then Result:=True;
end;
또는 한줄로 간단히
(Document as IPersistStreamInit).Save(TStreamAdapter.Create(Buf), True)
이렇게 하면 되겠죠.
파일로 저장할때는 TFileStream을 넣고 그냥 보여줄때는 TMemoryStream을 넣고 등등으로 하면 되겠네요.
--; 별로 복잡하지 않다고요? 원래는 IStream, TOleStream 등을 써서 아주 복잡하게 만들었었는데, 인터넷에 뒤져보니 저런 쉬운 방법이 있길래 저걸로 바꿨습니다.
반대로 스트림에 들어 있는 HTML을 웹브라우저로 집어 넣을때는
(Document as IPersistStreamInit).Load(TStreamAdapter.Create(Buf));
이렇게 하면 됩니다. 아주 간단하죠.
주) IHTMLDocument2 클래스를 사용하기 위해서는 MSHTML 을 uses 구문에 추가해야 합니다.
이 놈은 WebBrowser.Document 입니다. 혹은 WebBrowser.OleObject.Document
2.3. HTML 문서를 리소스에 넣기
BMP,WAVE 들도 다 리소스에 들어가는데 HTML문서라고 리소스에 들어가지 말란 법 있나요. HTML문서를 일단 밀어 넣어 보자구요.
TEST.RC 파일에다가
HTML1 23 "html\test1.htm" HTML2 23 "html\test2.htm"
이렇게 넣고 res로 컴파일합니다.
{$R TEST.RES}
이렇게 넣고
WebBrowser.Navigate('RES://' + Application.ExeName + '/HTML1');
이렇게 하면 된답니다. (저도 안 해봤습니다만) 안돼는걸 된다고 올려놓았을리는 없으니 틀림없이 되겠죠.
3. TWebBrowser말고 외부의 IWebBrowser2 인터페이스 얻어오기.
이 부분은 외부의 IE창의 웹브라우저를 조작하기 위한 부분입니다.
다 필요없고 우리가 필요한건 오직 IWebBrowser2 인터페이스 입니다.
이 놈만 얻어오면 내 프로그램의 컴포넌트처럼 조작할 수 있습니다.
전역 변수 혹은 폼 클래스 변수로 이놈을 선언 합니다. var FShellWindow : IShellWindows;
프로그램 초기화 루틴 혹은 폼 클래스 OnCreate 부분에 이걸 넣어 초기화 시킵니다.
FShellWindow := CreateComObject(CLASS_ShellWindows) as IShellWindows;
IShellWindows 는 윈도우상의 모든 쉘윈도우를 관리하기 위한 인터페이스입니다. CreateComObject 는 쉘윈도우 관리자를 새로 만드는게 아니라 기존의 관리자의 인터페이스를 얻어오게 됩니다. 왜냐하면 Single Instance 이기 때문에.
우리는 이제 필요할때마다 이 관리자에게 쉘에대해 물어 보면 그때그때의 열린 쉘들을 친절하게 가르쳐 줍니다. 여기서 쉘이란 IE만을 가르키는게 아닙니다. 윈도우즈탐색기, 제어판, IE창, IE - FTP모드 창 등의 모든 쉘들을 다 가르쳐 주기때문에 웹브라우저를 선별하는 작업을 따로 해주어야합니다.
Count := FShellWindow.Count; // 현재 열린 쉘들의 갯수 if Count>0 then
For I:=0 to Count-1 do begin
if WebBrowserCheck(FShellWindow.Item(I) as IWebBrowser2) then begin
// 이 놈은 웹브라우저 이다.
end;
end;
이런식으로... 여기서 선별하는 방식은 정석인지 아닌지 모릅니다. 그냥 제가 쓰는 방법을 적은 것일 뿐입니다. 간단하게 Document 인터페이스를 가지고 있는지 아닌지로 판단 합니다. 일단 탐색기나 제어판 등은 Document 인터페이스가 없습니다.
function WebBrowserCheck(const Webb : IWebBrowser2) : Boolean; var WebV : Variant; Buf : String; begin WebV := Webb; Result := False; if Assigned(Webb) then
try Buf := WebV.Document.URL;
// Document 인터페이스가 할당 되어 있지 않으면 예외오류 발생
Result := True;
except ;
end;
end;
해보지는 않았지만 어쩌면 FShellWindow.QueryInterface로 체크해보면 제대로된 IwebBrowser2를 걸러낼 수 있을지도 모르겠어요. 암튼 이렇게 IWebBrowser2를 얻어 왔다면 모든게 순조롭게 진행이 되겠죠.
예을 들어 그 IE 창을 닫아 버리고 싶다면?
Webb : IWebBrowser2; WebbApp : IWebBrowserApp; WebV : Variant; 일 경우
if Webb.QueryInterface(IID_IWebBrowserApp, WebApp)=S_OK then WebApp.Quit;
이렇게,
그 창의 URL을 얻어오고 싶다면
WebV := Webb; URL := WebV.Document.URL;
여기서 잠깐.. 왜 Variant 타입의 변수에 넣어서 엑세스 하냐면요. 그냥 하면 에러가 납니다. 왜냐면 Webb.Dcocument 는 IDispatch 형이기때문에 URL이란 프로퍼티가 없기 때문이죠. 물론 (Webb.Get_Document as IHTMLDocument2).URL 이렇게 해도 되겠죠. 뭐 이런건 대충 편한대로 알아서 하시기 바래요~
4. 마치면서.
--; 할말이 없음.
02 - IE확장자 - NameSpace
이거 참.. 쩝 오늘 작은 집에서 제사라는 연락이 와서 글루 바로 갈려고 사무실에서 시간을 떼우고 있습니다요. 12시에 제사니깐 11시까지는 있야하는데 할일두 없고 해서..
지금까지 IE에대해 공부하면서 끌어모은 자료들을 정리도 할 겸해서 강좌를 한번 해볼려고 합니다. 쉘/익스플로러 확장자에 관한건데요..
제가 일이 없을땐 몇날몇일 한가하다가 일생기면 죽어나기 때문에 규칙적으로 올리지는 못할것 같습니다. 틈나는 대로 올리도록하죠.
1. 확장자(Extensions)란?
뭐 별거없습니다. 일종의 플러그인 프로그램인데, 요즘 좀 한다하는 프로그램들은 모두 이런 확장자를 지원합니다. IE, 윈도우즈 탐색기, 미디어플레이어, 넷스케이프도 플러그인을 지원하구요, 델파이조차 지원을 합니다. 다들 나름대로의 규칙을가지고 있는데, 최근에 릴리즈된 마이크로소프트 제품은 모두 COM 방식으로 지원을 합니다.
미리 말씀드리지만 확장자를 제대로 만들기위해서는 델파이는 그리 추천할 만한 도구가 아닙니다. 코드의 사이즈도 크고, 메모리도 많이 잡아 먹으면서, 기본적으로 지원되지 않는 인터페이스도 많습니다. 이리저리 알아보니 확장자를 제대로 만들려면 역시 VC가 젤 낫겠더군요. 예전에 VC 한번 써볼려다가 Hello! 이 한 문장 출력하는 프로그램을 컴파일하는데 5분이 걸리는걸 보고 (당시 486이었음) 두번 돌려보지도 않고 바로 삭제해 버린 기억이 나는데, 요즘은 좋아졌나 모르겠습니다만... 어쨌던 배운게 도둑질인데, 델파이로도 한번 시도는 해봐야죠.
2. 미리 알아두기 (1)
이걸 만들려면 인터페이스의 개념을 알아야합니다. 아래 정훈님이 인터페이스에대한 강좌를 요청하셨는데, 저두 그러고 싶지만 제가 제대로 모르기때문에 엄두를 못냅니다. 이걸 만드는데는 인터페이스에 대해 자세히 알필요는 없습니다. 그냥 제가 아는 정도만 아시면 될것 같아요.
인터페이스(Interface)는 그냥 통신의 경로라고 생각하시면 될것 같네요. 내가 어떤 프로그램 또는 컴퍼넌트를 만들면서 다른 누군가가 내 프로그램에 쉽게 접근할 수 있는 길을 열어두는거죠. 예를들면 아래 IE팁란에 적어놓은 것들이 가능한건 IE가 그런 인터페이스들을 만들어 놓았기때문에, 그 길을 통해 IE를 조작하는 것이었죠.
하지만 여기서 해야할 일은 IE가 만들어 놓은 인터페이스를 이용하는것이 아니라, 바로 그 인터페이스를 만들어야합니다. IE 내부에서도 인터페이스들을 통해 모듈들이 의사소통을 하는데, 우리는 그 인터페이스를 만들어 둘이 의사소통하는 사이에 끼어들어야 합니다. 어찌보면 후킹이랑 개념이 또 비슷한것 같네요. 혹은 끼어드는것이 아니라 다른 인터페이스들과 나란히 줄맞취서서 IE/탐색기가 불러주기를 기다리기도 하죠.
예를들면 이런 일을 할수 있어요. 탐색기 상에서 오른쪽 버튼을 누르면 메뉴가 쭈욱 나오죠? 우리가 Context Menu를 제어하는 확장자를 하나 만들어 넣으면 탐색기가 내 프로그램에 와서 물어봅니다. "넌 뭐 끼워 넣을거 없니?" 그러면 내 프로그램은 대답 합니다. "바이러스를 검사하는 항목을 넣어주세요." 그러면 오른쪽 버튼 메뉴 목록에 "바이러스 검사"라는 메뉴가 생기는 겁니다. 그런데 탐색기가 내 프로그램에게 어떤 방식으로 물어 볼까요? 그게 바로 정해진 인터페이스입니다.
윈도우즈탐색기에서 오른쪽 버튼을 누르면 WinZip을 설치한 경우 "WinZip으로 압축풀기" 등의 항목이 들어가죠? 이게 바로 그 확장자 란겁니다. 거기 메뉴에 집어 넣는건 간단히 레지스트리 조작만으로도 추가할 수가 있습니다. ASPack.exe가 레지스트리만 추가해서 메뉴에 올라간 예가 되겠군요. 이 경우에는 파일의 확장명에 연결시키기때문에 exe 화일이면 무조건 ASPack 관련 메뉴가 튀어나옵니다. 하지만 WinZip은 다르죠. exe위에서 오른버튼을 누르면 윈집이 이게 zip으로 압축된 실행화일인지 아닌지 먼저 검사해서 "WinZip으로 풀기"가 나올수도 있고, 안나올수도 있습니다.
이제 우리는 이런 인터페이스들을 만들어서 탐색기 또는 IE가 나를 불러주기를 기다리기만 하면 됩니다.
3. 미리 알아두기 (2)
델파이는 COM을 지원합니다.
먼저 File / New... / ActiveX / ActiveX Library 를 선택합니다.
그리고, File / New... / ActiveX / COM Object 를 선택합니다.
Class Name 은 뭐 적당한 이름을 적어넣으시고, Multiple Instance, Apartment를 선택하고, Include Type Library의 체크표시를 지웁니다. Type Library를 만들어도 좋지만 괜히 복잡하기만 하고 별 필요가 없습니다.
그러면
unit Unit1;
interface
uses Windows, ActiveX, Classes, ComObj;
type TRedFilter = class(TComObject) protected end;
const Class_RedFilter: TGUID = '{A579EF45-4AB7-11D5-9C81-000102657B27}';
implementation
uses ComServ;
initialization TComObjectFactory.Create(ComServer, TRedFilter, Class_RedFilter,
'RedFilter', , ciMultiInstance, tmApartment);
end.
이렇게 기본 틀이 나오죠.
여기서 작업을 합니다.
작업을 한 후 컴파일을 하고는 이걸 레지스터리에 등록을 해야합니다.
Run 메뉴에 보면 Register ActiveX Server, UnRegister ActiveX Server 가 있는데 이걸로 레지스트리에 넣었다 뺐다 하면 됩니다. 이건 모든 강좌의 공통적인 사항입니다.
4. 확장자로 할수 있는일들
여기다가 확장자로 어떤일들을 할수 있는지, 자세하게 적어야하는데.. ^^; MSDN이 워낙 느리고 11시가 벌써 가까워진 관계로 좀 미뤄야 하겠군요.
단지, 제가 할수 있는 강좌는 몇부분 안됩니다.
1. 아래에 들어갈 IE NameSpace .. 이건 아래에 자세히 2. 탐색기 오른쪽 단추 메뉴 추가하기 3. IE 밴드(도구표시줄에 들어가는), 탐색창. 4. 기타 탐색기의 몇몇 확장자..
또는 계속 공부를 하면서 다른것들도 할수가 있구요. 일단은 시간이 없으니 첫번째 강좌로 넘어가지요.
5. IE NameSpace Handler
URL이 http:/www.delmadang.com/index.html 이렇게 되어있다면 여기서 www.delmang.com을 NameSpace라고 부르는것 같아요. (정확하지 않음) 이건 뭐하는 거냐하면 IE가 Navigating 을 하는 중간에 끼어 드는겁니다.
즉 내가만든 인터페이스를 통해 홈페이지를 불러오는거죠. 비슷한 개념으로 Pluggable Protocol 이란게 있는데, 이건 사용자 정의 프로토콜을 만들수가 있습니다. 즉 babo://... 이런 주소를 만들수가 있는거죠. 하지만 별로 써먹을곳이 없을것 같으니 강좌에는 생략합니다.
이건 어따가 써먹으면 좋을까요? 그렇습니다. 바로 음란물 차단 프로그램입니다. 이걸로 음란물 차단 프로그램을 만들면 DWebBrowser2 컨트롤을 이용하는(IWebBrowser2 인터페이스를 이용하는) 모든 프로그램들을 감시할 수 있습니다. 물론 넷스케이프는 이걸로 막을 방법이 없습니다. 대충알기로는 넷스케이프는 DDE를 이용한다고 하는데, 저는 DDE의 D자도 모르는 놈이기때문에 제 영역 밖입니다. 저한테 물어보지 마세요. Inet API를 후킹하는 것보다는 못하지만 그래도 간단하게 음란물 차단 프로그램을 만들 수 있습니다.
아는 사람은 다 아는 GetRight 라는 프로그램이 있습니다. 이 놈도 역시 이걸 사용합니다. 지가 받아서 지가 받을 항목이면 가로채는 거죠.
이 NameSpace 핸들러를 구현하기 위해 우리가 만들어야할 인터페이스는 IInternetProtocol 입니다. 계보를 따지면
IInternetProtocolRoot
|
IInternetProtocol
이렇습니다. 우리가 이 인터페이스를 구현해 놓으면 IE가 우리가 만든 함수들을 호출하는 방식으로 진행이 됩니다.
그러면 어떻게 코딩을 하는가..
type TRedFilter = class(TComObject) protected end;
이부분에 IInternetProtocol을 추가합니다.
TRedFilter = class(TComObject, IInternetProtocol)
바로 이렇게..
그리고 멤버함수들을 추가해야하는데.. 이때는 IInternetProtocol 글자위에 마우스 커서를 놓고 ctrl+클릭을 합니다. 아참.. 그전에 UrlMon 항목을 Uses 절에 추가해 주셔야 합니다. 거기에 정의되어 있으니깐요.
ctrl+click 하면
{$EXTERNALSYM IInternetProtocol} IInternetProtocol = interface(IInternetProtocolRoot)
['{79eac9e4-baf9-11ce-8c82-00aa004ba90b}']
function Read(pv: Pointer; cb: ULONG; out cbRead: ULONG): HResult; stdcall;
function Seek(dlibMove: LARGE_INTEGER; dwOrigin: DWORD; out libNewPosition: ULARGE_INTEGER): HResult; stdcall;
function LockRequest(dwOptions: DWORD): HResult; stdcall;
function UnlockRequest: HResult; stdcall;
end;
이렇게 적힌 부분이 튀어 나오죠? 이것들이 멤버함수 입니다.
일단 이놈의 function 4개를 복사해서 protected 영역으로 옮깁니다. 그리고 다시 이리로 돌아와서 IInternetProtocolRoot 위에 마우스를 대고 ctrl+click 합니다.
{$EXTERNALSYM IInternetProtocolRoot} IInternetProtocolRoot = interface
['{79eac9e3-baf9-11ce-8c82-00aa004ba90b}']
function Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink;
OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult; stdcall;
function Continue(const ProtocolData: TProtocolData): HResult; stdcall;
function Abort(hrReason: HResult; dwOptions: DWORD): HResult; stdcall;
function Terminate(dwOptions: DWORD): HResult; stdcall;
function Suspend: HResult; stdcall;
function Resume: HResult; stdcall;
end;
그러면 이부분이 나오는데 역시 복사해서 옮깁니다. 그러면 완성된 모양은
TRedFilter = class(TComObject, IInternetProtocol) protected { IInternetProtocolRoot}
function Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink;
OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult; stdcall;
function Continue(const ProtocolData: TProtocolData): HResult; stdcall;
function Abort(hrReason: HResult; dwOptions: DWORD): HResult; stdcall;
function Terminate(dwOptions: DWORD): HResult; stdcall;
function Suspend: HResult; stdcall;
function Resume: HResult; stdcall;
{ IInternetprotocol }
function Read(pv: Pointer; cb: ULONG; out cbRead: ULONG): HResult; stdcall; function Seek(dlibMove: LARGE_INTEGER; dwOrigin: DWORD; out libNewPosition: ULARGE_INTEGER): HResult; stdcall; function LockRequest(dwOptions: DWORD): HResult; stdcall; function UnlockRequest: HResult; stdcall;
end;
이렇게 되는데, 이제 이 함수들을 모두 만들어 주어야 합니다. 여기에는 쓸데없는 함수들도 있고, 꼭 필요한 함수들도 있습니다.
여기서 예제로 사용되는건 .COM 주소를 차단하는 프로그램입니다. .co.kr 등은 잘 접속되지만 .com 주소는 모두 차단합니다. 델마당까지도...
그러기 위해서는 다른 부분은 전혀 손델 필요없이 Start 함수만 꾸며주면 됩니다. 그렇다고 다른 함수들은 안만들어도 되느냐? 그건 아닙니다. 모든 멤버함수들을 다 만들어 주어야합니다.
function TRedFilter.Continue(const ProtocolData: TProtocolData): HResult; stdcall; begin Result := S_OK; // 무조건 성공했다고 보내자. end;
function TRedFilter.Abort(hrReason: HResult; dwOptions: DWORD): HResult; stdcall; begin Result := E_NOTIMPL; // 이건 원래 지원안되는 함수다. end;
등 처럼 그냥 리턴만 시켜주면됩니다.
그럼 Start는 뭐냐? IE가 이리로 접속해라! 하고 명령을 내리는 부분입니다.
function Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink;
OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult; stdcall;
그 URL 이 szUrl 로 넘어옵니다. 델파이형으로는 WideString 형식입니다. 기타 IInternetProtocolSink, IInternetBindInfo 는 Pluggable Protocol 을 구현하자면 필요할것이고, 또 좀더 복잡하게 만들어서 Read 함수까지 정의해주고 싶다면 필요합니다. 하지만 여기 간단 예제에서는 그냥 무시하기로 하지요.
function TRedFilter.Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink;
OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult; stdcall;
begin
if CheckRed(szUrl)
then Result := INET_E_DATA_NOT_AVAILABLE // 이 사이트는 사용할수 없다고 속이자. else Result := INET_E_USE_DEFAULT_PROTOCOLHANDLER; // 내가만든 핸들러 통하지말고 원래 핸들러로 알아서 해라.
end;
이렇게 간단히 코딩합니다.
URL이 검사해서 통과시킬 URL이면 INET_E_USE_DEFAULT_PROTOCOLHANDLER 을 돌려주면 내가만든 인터페이스를 통하지않고 지가 알아서 합니다. 그렇기때문에 우리는 여기서 나머지 멤버함수들을 자세히 만들어줄 필요가 없는것입니다.
(시간없어 바빠 죽겠는데 왠 이름들이 이리긴지 몰겠군요... 복사해서 붙여넣기도 힘드네..)
여기서 S_OK를 돌려보내고 싶다면 다른 부분들도 세팅을 해야합니다. 계속 나한테 와서 물어보기 때문에...
그리고는 저 CheckRed 함수를 따로 만들어 주면 되겠죠? 어떤 사이트를 차단할것이지에 따라.. 음란물을 차단하고 싶으면 구축된 DB에 퀴리를 날려서 가부를 결정하면 될테구요.
function TRedFilter.CheckRed(const URL : String) : Boolean; begin if Pos('.COM', UpperCase(URL)) > 0 // .com 이라는 글자가 들어간 사이트는 무조건 차단!!
then Result := True else Result := False;
end;
이것으로 완성입니다.
자.. 컴파일하고 레지스트리에 등록하고 익스플로러를 돌려봅시다. 그리고 델마당에 접속해보세요. 냐하하 잘 접속이 되지요.. --; 뭔가 빠진 부분이 있기 때문입니다.
레지스트리에 등록을 하긴 했지만 그건 COM 서버로 등록을 한것이고.. 이게 하는 일은 NameSpace Handler 이다..라는걸 또 등록해 주어야합니다..
[HKEY_CLASSES_ROOT\PROTOCOLS\Name-Space Handler\http\RedFilter] "CLSID"="{A579EF45-4AB7-11D5-9C81-000102657B27}" "Pattern1"="" @="RedFilter"
여기서 CLSID는 내가 만든 클래스의 CLSID를 적어주면 됩니다. 프로그램 인스톨과정에서 등록하면 되겟죠... 이 예제에서는 RedFilter.reg 화일이 첨부되어 잇으니 그걸 실행하세욥.
여기서 잠깐... Pattern 이란 항목이 있는데, 이건 IE의 버그로 아무리 적어줘도 소용이 없답니다. 뭔지 알아볼려고 시간 낭비 하지 마세요.
이제 다시 익스플로러를 띄어보세요. ^^; 결과가 나오죠. (캐쉬가 되어 있는 사이트의 경우 그냥 보여지는 수도 있는걸 보면 캐쉬가 우선 되나 봅니다.)
프로그램을 수정해서 다시 컴파일 하기위해서는 모든 익스플로러를 닫아야합니다. 하이텔2000, 소리바다 같은 WebBrowser컨트롤을 사용하는 놈도 모두 닫아야합니다. 안그러면 사용중이라고 컴파일이 되질 않습니다.
그리고, 테스트를 마칠때에는 꼭 Run / Unregister ActiveX Server 를 실행시키도록 하세요. 안그러면 절대 델마당으로 못옵니다.
6. 오늘은 요까지..
훗 거의 11시가 다 됐네요.. 예제 압축해서 올리면 시간이 꼭 맞겠군요.. 그럼 또 담에 시간 날때 뵙죠~
03 - 쉘 확장자 - ContextMenu Handler
1. 들어가기 전에
지난 강좌는 IE의 확장자 였죠. 이번에는 IE가 아니라 윈도우즈 탐색기의 확장자 입니다. 헷갈리지 마세요.
2. ContextMenu 확장자란..
탐색기 상에서 오른쪽 버튼을 누르면 나오는 바로 그 메뉴입니다. 지난 강좌에서 잠깐 언급 했지만, 단순히 레지스트리를 조작해서 추가하는 메뉴와 확장자의 차이를 잠깐 짚고 넘어가도록 하죠.
확장자로 제작된 메뉴는 레지스트리 조작에 비해 훨씬 정밀하게 조작이 가능합니다.
1) 여러 화일을 선택해서 오른쪽 버튼을 누를 경우 레지스트리 조작만으로는 메뉴 추가가 불가능 합니다. 2) 레지스트리 조작은 단순히 화일 확장명과 연결된 것이므로 확장명을 임의로 바꿀 경우 레지스트리를 또 조작해야 합니다. 3) 레지스트리 조작의 경우 연결된 확장명이 많으면 그만큼 레지스트리를 많이 손봐야 합니다.
WinZip, WinRar, 알집 등 거의 모든 압축프로그램들이 이러한 이유로 이 확장자를 사용합니다.
3. 준비 사항
오늘 만들어 볼 예제는 오른쪽 버튼 메뉴에 V3로 바이러스를 검사하는 항목입니다. V3 Pro 나 터보백신 등을 설치하면 자동으로 항목이 추가되지만, 도스용 V3.exe를 쓰는 사람에게는 이게 나오지 않죠. "바이러스 검사" 라는 한 가지 항목만을 추가하고, 멀티셀렉트를 지원하지 않는 (도스용 V3가 지원하기에는 너무 벅차기때문에 --;) 간단한 메뉴입니다. 단, V3.EXE가 Path 경로에 포함된 디렉토리에 있어야 합니다. 기능은 폴더/드라이브 위에서 오른쪽 버튼, 실행화일 위에서 오른쪽 버튼을 누르면 메뉴에 "바이러스 검사"라는 항목이 표시됩니다. 이때 실행화일은 꼭 EXE화일만이 아니라 SCR, CPL, DLL, OCX, DRV, VXD 등 "MZ" 코드로 시작되는 모든 화일을 가리킵니다. EXE 화일의 확장명을 TXT로 바꿔도 메뉴가 표시됩니다. 단순히 확장명만 검사하는게 아니라 코드를 검사하기 때문이죠. (DOS의 COM,SYS 화일은 감지하지 못합니다.)
프로그램의 시작은 지난 강좌와 동일합니다. 프로젝트를 같은 방법으로 열고 IShellExtInit, IContextMenu 두 인터페이스를 상속 받습니다. (상속받는다는건 적당한 표현이 아닌것 같네요.. 그래도 문법상 상속받는것 처럼 보이니깐.. 실제로는 두 인터페이스를 내가 구현하겠다는 걸 알리는 겁니다.) 그리고 두 인터페이스들의 멤버 함수들을 지난 강좌와 같은 방식으로 복사해 넣습니다. 오늘은 모두 4개의 함수를 만들어야 하겠군요. 참고로 두 인터페이스를 사용하기 위해서는 ShlObj 유닛이 Uses절에 포함되어야 합니다.
여기서 잠깐.. 아주 사소한 문제가 한가지 발생합니다. IShellExtInit 인터페이스는 Initialize 라는 멤버함수를 가지는데, 우리가 상속받은 TComObject 클래스(이건 정말로 상속받은거임)에도 같은 이름의 함수가 존재합니다. 이걸 그냥 놔두면 델파이가 아주 혼란스러워 하기때문에 아래와 같이 살짝 이름을 바꾸어 줍니다. 완성된 모양은 이렇게 됩니다.
TDosVirus = class(TComObject, IShellExtInit, IContextMenu) private
FSelectedFile : Array[0..MAX_PATH] of Char; function CheckExecutableFile : Boolean;
protected {IShellExtInit}
function IShellExtInit.Initialize = ShellInitialize; // 이름 충돌 때문에..
function ShellInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall;
{IContextMenu}
function QueryContextMenu(Menu: HMENU;
indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;
여기서 private 영역은 프로그램 진행상에 필요한 부분이라 따로 넣었습니다. 이제 하나 하나 살펴 보도록 하지요.
4. 만들자!
IShellExtInit.Initialize ( ShellInitialize로 알리아싱 된 )는 마우스 오른쪽 버튼이 눌릴때 내 프로그램이 선택된 화일명을 넘겨받는 역할을 합니다. 여러 다른 쉘확장자에서도 화일명이 전달될 필요가 있을경우에는 이 인터페이스를 이용하게 됩니다. 전달하는 방식은 좀 복잡합니다. 간단히 String 형으로 화일명을 넘겨 주면 쉬울텐데, 여러가지 복잡한 구조로 되어 있습니다. 파일을 드러그&드롭할때 파일명을 넘겨주는 방식으로 전달이 되는데요. 다시말하면 선택된 파일을 끌어서 내 프로그램에 던져 넣는다고 이해 하시면 되겠습니다.
function TDosVirus.ShellInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall; var Medium: TStgMedium; FormatEtc: TFormatEtc; begin Result := S_FALSE; if lpdobj = nil then Exit;
with FormatEtc do // 드롭된 파일명을 풀어내는데 필수적인 요소.. 그냥 있는대로 쓰자. begin
cfFormat := CF_HDROP; ptd := nil; dwAspect := DVASPECT_CONTENT; lindex := -1; tymed := TYMED_HGLOBAL;
end;
if Failed( lpdobj.GetData( FormatEtc, Medium ) ) then Exit;
try
if DragQueryFile( Medium.hGlobal, $FFFFFFFF, nil, 0) = 1 then begin
DragQueryFile( Medium.hGlobal, 0, FSelectedFile, MAX_PATH);
Result := S_OK;
end;
finally
ReleaseStgMedium( Medium );
end; end;
다른 부분은 그러려니 하고 넘어가셔도 좋지만, try 안의 구문은 이해를 하셔야 할 것 같아요. DragQueryFile함수의 두번째 인자에 $FFFFFFFF 를 집어 넣으면 선택된 화일의 갯수가 리턴됩니다. 이 예제에서는 단 한개가 선택된 경우만 취급하지만 멀티 셀렉트도 받아들이려면 조금 고쳐야 하겠죠? 그 두번째 인자에 0을 넣으면 선택된것 중에 첫번째 파일명을 얻어오고, 1을 넣으면 두번째 파일명을 얻어오고, 기타등등.. 일케 됩니다.
이번에는 QueryContextMenu에 관해 알아보죠. 탐색기가 내 프로그램에게 물어보는 인터페이스입니다. "메뉴에 추가실킬게 있으면 추가해라" 하고 말이죠.
function TDosVirus.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall; begin Result := S_OK; if (( uFlags and CMF_DEFAULTONLY )=0) and (FSelectedFile[0]<>#0) then begin
if FileExists(String(FSelectedFile)) then
if not CheckExecutableFile then Exit;
InsertMenu( Menu, indexMenu, mf_String or mf_ByPosition,
idCmdFirst, PChar('바이러스 검사 해보기') );
Result := 1;
end;
end;
uFlags 옵션은 여러가지의 조건을 담고 오는데, 여러 잡다한 것들은 MSDN을 참고하시구요, 여기에 쓰인 CMF_DEFAULTONLY 는 오른쪽 버튼 메뉴가 물어보는게 아니라 디폴트액션 그러니까 더블클릭(원클릭 실행옵션을 쓰는 경우에는 원클릭)을 했을때, 넘어 오는 값입니다. 보통은 확장명에 연결된 실행화일이 실행되는데, 이 쪽으로도 넘어오는 것 같아요. (테스트를 해보지 않아 연결프로그램이 우선인지 이게 우선인지는 알지 못해요.) 암튼 위의 문장을 해석하면 실행 액션을 제외한 모든 경우에 행동을 개시하라 라고 되겠네요. FSelectedFile[0]<>#0 이 부분은 넘어오긴 넘어오는데 선택된 화일이 없는 경우가 있습니다. 예를들어 제어판아이콘, 네트워크환경 아이콘등의 특수 항목위에서 오른쪽 버튼을 클릭한 경우죠. 그런 경우를 걸러내구요.
if FileExists(String(FSelectedFile)) then if not CheckExecutableFile then Exit; 이 부분은 만약에 폴더나 드라이브가 아닌 화일이 넘어온 경우에 그게 실행 화일인지 아닌지 판단하는 부분입니다. CheckExecutableFile 은 대상화일이 실행화일이면 True를 아니면 False를 리턴합니다.
InsertMenu 는 API 함수고 사용법도 간단하죠. 여기서는 1개의 항목만을 추가했지만 2개 이상의 항목을 추가할 수도 있습니다. 이때 두번째 항목은 idCmdFirst+1, 세번째 항목은 idCmdFirst+2 등으로 추가하시면 되겠습니다.
리턴값은 추가한 항목의 갯수가 되겠습니다. 여기서는 하나의 항목만 추가했으니 Result := 1;
GetCommandString 함수는 여러가지 부수적인 사항에대해 대답해 주는 일을 합니다.
function TDosVirus.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult; stdcall; begin if idCmd = 0 then begin
if uType = GCS_HELPTEXT
then StrLCopy(pszName, PChar('검사 대상 : '+FSelectedFile), cchMax);
Result := NOERROR;
end
else Result := E_INVALIDARG; end;
이 예제에서는 GCS_HELPTEXT에 관해서만 다루었는데, 이건 마우스 커서가 내 메뉴항목 위로 올라올때 상태표시줄에 표시할 문자열을 달라는 요청입니다. idCmd는 내가 만든 메뉴중 몇 번째 메뉴를 묻는건지가 들어 있는데, 차례로 0,1,2,.. 이렇게 됩니다. 좀더 치밀하게 하려면 GCS_HELPTEXTA : 앤시코드, GCS_HELPTEXTW : 유니코드.. 일케됩니다. 기타 언어(IME)에 종속적인 명령 어쩌고 저쩌고 등의 몇 개가 더 있는데 필요는 없을것 같아요.
이제 순수한 알맹이 InvokeCommand 만 남았네요. 이건 메뉴를 펼치고 내 메뉴를 실행할때 호출이 됩니다. 클릭의 정보들이 괴상한 구조체에 담긴체 말이죠.
function TDosVirus.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall; const V3 = 'V3'; // V3가 있는 디렉토리가 Path 경로에 포함되어 있어야 한다. var HR : Integer; begin HR := ShellExecute(0, 'open', V3, PChar(FSelectedFile+' /S'), FSelectedFile, SW_NORMAL); case HR of
0 : ShowMessage('메모리 또는 리소스가 부족해.');
2 : ShowMessage('화일을 찾을수 없어. ('+V3+')');
3 : ShowMessage('대상 폴더가 없어.');
1,4..32 : ShowMessage('어쨌던 실행할 수 없어.');
end;
Result := S_OK; end;
이 예제에서는 이것저것 조건 안따지고 무조건 V3를 실행시켜 버리는걸로 만들었는데요, 조건을 따지면 좀 더 복잡해 집니다.
여러 개의 메뉴항목이 추가된 경우 몇번째 메뉴가 클릭된건지를 알아야 하겠죠? 그 정보는 lpici.lpVerb 에 담겨져 옵니다. 넣을만한 적당한 곳이 없어서 그런건지 어떤지 헷갈리게스리 포인터형에다가 담아 오죠. 것두 유니코드인지 아닌지에따라, TCMInvokeCommandInfo로 넘어왔지만 사실은 좀 더확장된 TCMInvokeCommandInfoEx 인지 아닌지에 따라 등등 복잡합니다. 자세한건 역시 MSDN을 참고하시고 ( 무책임한 ^^; )... 그냥 LoWord(lpici.lpVerb) 의 값을 들여다봐서 0이면 첫번째 메뉴클릭, 1이면 두번째 클릭 등등.. 으로 하시면 되겠습니다.
이렇게 코딩은 끝입니다.
여러분이 원한다면 여기다 New Form을 해서 여러가지 폼을 만들어, 메뉴가 선택될 경우 외 프로그램을 부르는게 아니라 바로 폼을 띄우는 형식으로 구성해도 상관은 없습니다. 그러나 그러나.. 쉘 확장자 프로그램은 얼마나 덩치가 작은가 얼마나 처리 속도가 빠른가 하는게 생명이란걸 염두에 두시기 바래요. 오른쪽 버튼을 눌렀는데 한참있다 메뉴가 그려지는 상황을 연출하고 싶지않다면 메인 모듈은 따로 빼내는게 정석이겠죠?
5. 레지스트리에 등록하자!
기본적으로 COM 등록은 다들 아시죠? 델파이 Run메뉴의 Register ActiveX Server로 등록을 합니다.
그 COM을 이제 ContextMenu 확장자다 하고 등록을 해야해요. EXE 파일만 다루는 거라면
HKEY_CLASSES_ROOT\.exe 아래에만 등록하면 되지만 지금 이 예제는
- 모든 확장명과 연결한다.
[HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\DosVirusExt] @="{2B7FDFA0-4DCE-11D5-9C81-000102657B27}"
- 디렉토리 상의 오른 클릭도 연결한다.
[HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\DosVirusExt] @="{2B7FDFA0-4DCE-11D5-9C81-000102657B27}"
- 드라이브 문자 위의 오른 클릭도 연결한다.
[HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\DosVirusExt] @="{2B7FDFA0-4DCE-11D5-9C81-000102657B27}"
- 폴더위의 오른 클릭도 연결한다. --; 디렉토리랑 뭐가 다른걸까..
[HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\DosVirusExt] @="{2B7FDFA0-4DCE-11D5-9C81-000102657B27}"
이렇게 모두 4곳에 등록을 합니다. 모두 하위의 "shellex\ContextMenuHandlers\내프로그램이름" 처럼 등록하면 되겟습니다.
이 예제에서는 역시 REG 화일이 포함되어 있으니 그냥 실행만 하시면 됩니다.
( 테스트할땐 일케하면 되지만 배포할땐 어떻게 하냐구요? 그냥 잘 하세요.. --; 인스톨실드에서 아마 레지스트리 조작 기능을 지원할테구요. 그런류의 프로그램을 안쓰는 경우 따로 셋업파일을 첨부하거나 메인 프로그램의 옵션이 알아서 하거나 등등..)
6. 여기까지 입니다.
기타 참고사항은
http://msdn.microsoft.com/library/psdk/shellcc/shell/Shell_Int/ContextMenuHandlers.htm
(짱나는 MS홈페이지 광고 해주는것 같아 쓰고싶지 않았지만 ..)
04 - IE확장자 - Tool Band
1. 툴밴드(Tool Band)란?
이건 IE의 확장자입니다. IE의 상단에 CoolBar가 보이시죠? 그 안에 여러개의 ToolBar들이 들어 있는데, 거기다가 내가 만든 툴바를 집어 넣고 싶을때 쓰는겁니다. 보기/도구모음 메뉴에서 (또는 쿨바 위에서 오른클릭) 툴바들을 보이고 감추고 할 수 있죠.
이 놈과 유사품으로 Explorer Bar와 Desk Band가 있습니다. Explorer Bar는 보기/탐색창 메뉴에서 선택되는 항목으로 브라우저의 왼쪽에 세로로 튀어 나옵니다. Desk Band는 IE 확장자가 아니라 윈도우즈 확장자라고 해야 하나요? 모니터 젤 아래쪽에 작업표시줄이 있죠? (시작버튼이 있는 줄) 거기에 들어가는 밴드입니다. 이 둘은 오늘 강좌의 주메뉴인 ToolBand와 거의 똑같습니다. MSDN에도 세 놈을 같이 취급하고 있구요.
아마도 오늘은 상당히 복잡하고도 긴 글이 될것 같은데요. 예제로 만들어 볼 밴드는 델마당 검색밴드입니다. 이름을 델밴드라고 붙였는데요(대일밴드가 아님) 델마당 홈페이지 왼쪽 상단에 게시판 검색 폼이 있죠? 그 놈과 같은 기능을 하는데, 이건 밴드로 붙는 겁니다.
2. 들어가기 전에...
오늘은 확장자들을 좀 더 고급스럽게 등록하는 방법을 한 번 살펴보도록 하겠습니다. 지금까지는 COM 등록 따로(델파이메뉴에서), 확장자 등록 따로(REG 화일로) 했었는데요, 이제는 한 방에 모조리 등록해 버리는 방법을 알아보도록 하지요. 처음에 마법사로 프로그램 시작 준비를 하면, 맨 아래쪽에
initialization TComObjectFactory.Create(ComServer, TDelSearch, Class_DelSearch,
'DelSearch', , ciMultiInstance, tmApartment);
end.
이런게 하나 생겼죠? 도데체 이게 뭐하는 놈인지 궁금하지 않으셨나요? 컴포넌트 공장? 이 TComObjectFactory 라는 클래스는 프로그램 동작시에 필요한 게 아닙니다. 바로 설치/제거 시에 사용되는 건데요. 델파이메뉴에서 Register ActiveX Server 또는 Unregister ActiveX Server를 누르면 바로 이 공장이 가동되는 겁니다. 혹은 도스창에서 Regsvr32.exe 로 설치할 경우도 마찬가지 입니다. (델파이도 아마 이걸 이용할지도..) TComObjectFactory 는 기본적인 기능 즉, COM 서버로 등록하고 제거하고 하는 기능 밖에 없기때문에 다른 부분의 레지스트리도 같이 조작하기 위해서는 공장을 따로 만들어야합니다. 간단히 TComObjectFactory 를 상속받아 만들면 되는데요, 오늘 예제 프로그램에 들어가 있는 공장은 이렇습니다.
type ComFacProc = procedure;
TComFac = class (TComObjectFactory) private
FInstallProc, FUnInstallProc : ComFacProc;
public
constructor Create(ComServer: TComServerObject; ComClass: TComClass;
const ClassID: TGUID; const ClassName, Description: string;
const InstallProc, UnInstallProc : ComFacProc;
Instancing: TClassInstancing; ThreadingModel: TThreadingModel = tmSingle);
procedure UpdateRegistry(Register: Boolean); override;
end;
복잡한것 같지만 실은 아주 단순합니다. 모든 기능을 그대로 상속 받으면서 설치시 실행될 함수, 제거시 실행될 함수를 각각 추가해 준것 뿐입니다. 이 예제에서는
procedure InstallProc; begin REG_SetString(HKEY_CLASSES_ROOT, 'CLSID\' + GUIDToString(Class_DelSearch), , BandCaption); REG_SetString(HKEY_CLASSES_ROOT, 'CLSID\' + GUIDToString(Class_DelSearch) + '\Implemented Categories\' + CATID_CommBand, , ); REG_SetString(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\Internet Explorer\Toolbar', GUIDToString(Class_DelSearch), ); end;
procedure UnInstallProc; begin REG_Delete(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\Internet Explorer\Toolbar', GUIDToString(Class_DelSearch)); end;
initialization TComFac.Create(ComServer, TDelSearch,
Class_DelSearch, 'DelSearch', ,
InstallProc, UnInstallProc, // 이 라인만 원래꺼와 다르죠.
ciMultiInstance, tmApartment);
end.
이렇게 하면 원래의 COM 설치/제거 기능은 알아서 하고 저 덧붙인 함수들을 같이 수행하는 겁니다. 그런 이유로 오늘 부터는 REG 화일이 따라가지 않을 것입니다. REG_xxx 함수들은 ComFac 유닛에 같이 포함되어 있습니다.
3. 레지스트리에 등록
원래는 이게 뒤쪽에 나와야 하는데, 위에 공장 설명부분에 레지스트리 부분이 나왔기때문에 그냥 한꺼번에 해버릴려구요.
const CATID_DeskBand = '{00021492-0000-0000-C000-000000000046}'; // 태스크바 밴드 (DeskBand) CATID_InfoBand = '{00021493-0000-0000-C000-000000000046}'; // 탐색창 (ExplorerBar) CATID_CommBand = '{00021494-0000-0000-C000-000000000046}'; // 툴바 밴드 (ToolBand)
툴 밴드는 위의 공장처럼 HKEY_CLASSES_ROOT\CLSID\{Your Band Object's CLSID GUID}\Implemented Categories\{00021494-0000-0000-C000-000000000046} (Key) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Toolbar\{Your Band Object's CLSID GUID} = (Value) 이렇구요.
탐색창은 HKEY_CLASSES_ROOT\CLSID\{Your Band Object's CLSID GUID}\Implemented Categories\{00021493-0000-0000-C000-000000000046} (Key) HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Explorer Bars\{Your Band Object's CLSID GUID} (Key) HKEY_CLASSES_ROOT\CLSID\{Your Band Object's CLSID GUID}\Instance\CLSID = '{4D5C8C2A-D075-11D0-B416-00C04FB90376}' (Value) HKEY_CLASSES_ROOT\CLSID\{Your Band Object's CLSID GUID}\Instance\InitPropertyBag\Url = '탐색창의 컨텐츠가 있는 페이지의 URL' (Value) 을 각각 집어 넣습니다.
데스크밴드는 어떻게 등록하는지 모르겠군요. (--;) MSDN 설명서에는 이 부분이 없어요. 아시는 분은 꼬리를 달아주시길 바래요.
실제 MSDN예제들을 보면 저렇게 무식하게 레지스트리를 직접 건드리는게 아니라 준비된 인터페이스들 및 함수들을 이용해서 등록합니다. 이번 강좌에서 그걸 흉내 한번 내볼려다가 기능은 같은데 쓸데없이 복잡해지기만 할것 같아 그만 뒀습니다. 첨부파일에는 ComCat 이라는 유닛으로 포함은 되어 있지만 예제에서 사용은 하지 않습니다. 참고만 하세요.
4. 시작하자.
지금까지와 같은 방법으로 새 프로젝트를 열고 COM공장부터 뜯어 고칩니다. 물론 그냥 두고 REG를 따로 세팅하는 방식으로 하셔도 상관은 없습니다. 그리고는 그대로 두고 File/"New Form" 메뉴를 누릅니다. 그렇습니다. 여태까지는 폼이 없는 확장자들이었지만 이건 폼이 필요합니다. 밴드를 넣으려면 밴드를 그려야죠.
ToolBand의 경우 ..
1. 폼에 CoolBar를 집어 넣는다. 2. CoolBar에 ToolBar를 집어 넣는다. 3. ToolBar에 놓고싶은 컨트롤들을 가져다 놓는다. (고수들은 ToolBar없이 CoolBar위에 바로 컨트롤을 올리기도 한다.) 4. 모양을 다듬는다. (폼의 BorderStyle는 bsNone로 하자)
크기를 알맞게 조정하고 (AutoSize를 이용하면 편리하다) 폼의 Visible이 True로 되어있는지 확인도 하자. (누구처럼 그림 안나온다고 혼자 열받지 말자)
DeskBand, ExplorerBar의 경우는 해보질 않아 꼭 집어 말씀드리지는 못하겠구요. DeskBand는 뭐 ToolBand랑 똑 같을것 같구, ExplorerBar는 패널이나 아무컨트롤을 가져다 놓고 align을 alClient로 주면 되지 않을까 싶습니다.
자 이제 폼을 만들었으니, 메인유닛으로 돌아와서 그 폼의 유닛을 uses 절에 추가합니다. 추가하는 김에 ComObj, ShlObj, Shdocvw 등 필요한 유닛들도 모두 포함시킵니다.
이 세 개의 확장자들은 똑같이 IObjectWithSite, IPersistStream, IDeskBand 인터페이스를 구현합니다. 여기에 추가해서 "입력을 요구하는 컨트롤"이 폼에 존재할 경우에는 IInputObject 를 추가로 구현해야하고, 메뉴 항목에도 포함시키고 싶다면 IContextMenu를 또 추가해야합니다. 오늘 예제로 사용되는 놈은 이렇습니다.
TDelSearch = class(TComObject, IObjectWithSite, IPersistStream, IDeskBand, IInputObject) private
Band : TFormBand; // 앞에서 만든 폼이 Create 될 자리 WinHandle : HWND; // IE CoolBar의 핸들을 보관할 자리 InputObject : IInputObjectSite; // 입력받는 부분이 있을경우 처리해야할 인터페이스 HasFocus : Boolean; // 지금 내가 포커스를 가지고 있는지 아닌지? WebBrowser : IWebBrowser2; // 웹브라우저
protected {IObjectWithSite}
function SetSite(const pUnkSite: IUnknown ):HResult; stdcall; function GetSite(const riid: TIID; out site: IUnknown):HResult; stdcall;
{IPersist}
function GetClassID(out classID: TCLSID): HResult; stdcall;
{IPersistStream}
function IsDirty: HResult; stdcall; function Load(const stm: IStream): HResult; stdcall; function Save(const stm: IStream; fClearDirty: BOOL): HResult; stdcall; function GetSizeMax(out cbSize: Largeint): HResult; stdcall;
{IOleWindow}
function GetWindow(out wnd: HWnd): HResult; stdcall; function ContextSensitiveHelp(fEnterMode: BOOL): HResult; stdcall;
{IDockingWindow}
function ShowDW(fShow: BOOL): HResult; stdcall; function CloseDW(dwReserved: DWORD): HResult; stdcall; function ResizeBorderDW(var prcBorder: TRect; punkToolbarSite: IUnknown; fReserved: BOOL): HResult; stdcall;
{IDeskBand}
function GetBandInfo(dwBandID, dwViewMode: DWORD; var pdbi: TDeskBandInfo): HResult; stdcall;
{IInputObject}
function UIActivateIO(fActivate: BOOL; var lpMsg: TMsg): HResult; stdcall; function HasFocusIO: HResult; stdcall; function TranslateAcceleratorIO(var lpMsg: TMsg): HResult; stdcall;
end;
정말 만들어야할 함수들이 무지하게 많지요. 이걸 조목 조목 다 설명하려니 앞이 다 캄캄합니다요. 그래서 대충대충(^^;) 넘어가도록 하겠습니다.
먼저 쓸데없는 놈들부터 뽑아서 버리고 지나가죠. 입력받는 컨트롤이 있을경우 IInputObject를 반드시(굵은글씨) 구현하라고 되어 있길래 집어 넣었는데, 디버깅 결과 IE가 전혀 호출을 하지 않습니다. 왜 만들라고 했는지 모르겠군요 T.T 그래도 예제에는 원래 예제(Visual C용)를 그대로 델파이로 옮겨 놓았습니다.
IPersistStream도 이 예제에서는 써먹지 않습니다. MSDN이 말하길 ExplorerBar의 경우 데이터를 스트림으로 받아내거나 집어넣을때 써먹을수 있다고 합니다. 써먹든 말든 무조건 디폴트 리턴값을 돌려주면 되겠습니다.
그럼 이제는 핵심부를 살펴보도록 하죠.
SetSite란 놈은 초기화/마무리 부분이라고 생각하시면 되겠습니다. 초기화시에는 pUnkSite에 값이 담겨오고 마무리 부분에서는 nil이 넘어 옵니다.
function TDelSearch.SetSite(const pUnkSite: IUnknown ):HResult; stdcall; const IID_IOleWindow : TGUID = '{00000114-0000-0000-C000-000000000046}'; // 델파이는 이런 기본적인 것도 const로 만들어 놓질 않았다. IID_IInputObjectSite : TGUID = SID_IInputObjectSite; var OW: IOleWindow; begin if Assigned(InputObject) then begin // 이게 할당 되어 있다면 마무리 호출이겟죠? // InputObject._Release;
InputObject := nil; end;
Result := E_FAIL; WinHandle := 0; if Assigned(pUnkSite) then // IE CoolBar의 핸들을 얻어오기 위한 작업 if SUCCEEDED(pUnkSite.QueryInterface(IID_IOleWindow, OW)) then begin
OW.GetWindow(WinHandle);
// OW._Release;
end;
if WinHandle<>0 // 쿨바가 없거나 마무리 호출이면 WinHandle=0
then Result := S_OK else Exit;
WebBrowser := nil;
// 이건 InputObject 인터페이스를 얻어오는 부분..
if FAILED(pUnkSite.QueryInterface(IID_IInputObjectSite, InputObject)) then begin
Result := E_FAIL;
Exit;
end;
// 이건 웹브라우저의 인터페이스를 얻어 오는 부분
if FAILED(((pUnkSite as IOleCommandTarget) as IServiceProvider).QueryService(IWebbrowserApp, IWebbrowser2, WebBrowser))
then WebBrowser := nil;
if not Assigned(Band) then begin // 폼을 생성시키자. 단, CreateParented 로 생성할 것. Parent는 IE의 CoolBar.
Band := TFormBand.CreateParented(WinHandle, WebBrowser); end;
if not Assigned(Band) then Result := E_FAIL; end;
위에 보시면 _Release 부분이 모두 리마크가 되어 있죠? 원래는 다 _Release해 줘야하는데 델파이는 지가 알아서 한다고 프로그램에서 호출하지 말라고 합니다. 얼마나 잘 알아서 하는지 두고 보자구요. 실제로 _Release를 시키면 당장은 문제가 없는데, 프로그램 종료부분에서 문제가 발생합니다. 제가 이것땜에 엄청 고생을 했죠.
function TDelSearch.GetSite(const riid: TIID; out site: IUnknown):HResult; stdcall; 단지 QueryInterface의 역할을 하도록 만들라고 합니다. 위에서 받아낸 InputObject가 쓰이는 유일한 곳. 소스는 생략합니다.
function TDelSearch.GetWindow(out wnd: HWnd): HResult; stdcall; 내가 만든 폼의 핸들을 요청하는 통로입니다. 폼이 아직 안만들어 졌으면 Create하고, 핸들을 wnd에 넘겨주면 됩니다. 역시 소스 생략.
function TDelSearch.ShowDW(fShow: BOOL): HResult; stdcall; 내 밴드를 숨겨라/보여라 하는 명령이 전달되는 통로입니다. True가 들어오면 보여주고, False가 들어오면 감추면 됩니다. 죽어도 감추기 싫다면 그냥 뻐팅겨도 됩니다.
function TDelSearch.CloseDW(dwReserved: DWORD): HResult; stdcall; 내 밴드를 뽀개라는 명령이 전달되는 통로입니다. 조용히 Destroy 하면 됩니다.
function TDelSearch.GetBandInfo(dwBandID, dwViewMode: DWORD; var pdbi: TDeskBandInfo): HResult; stdcall; 이건 정말 손이 많이 가는 부분입니다. IE가 내 밴드에게 와서 이것저것 꼬치꼬치 물어보는 부분이기 때문입니다. 귀찮다고 안가르쳐주면 안되고 하나하나 다 가르쳐 줘야합니다. 질문은 pdbi.dwMask 에 담겨 옵니다. DBIM_MINSIZE : 최소 사이즈가 얼마니? (IE창이 내 밴드 최소사이즈보다 더 작을 경우는 어쩔수 없이 무시됩니다.) DBIM_MAXSIZE : 최대 사이즈가 얼마니? (-1로 지정하면 제한이 없어집니다.) DBIM_INTEGRAL : 사이즈가 변할때 원하는 간격은 얼마니? (그리드 효과를 연상하시면 됩니다. 보통은 1이죠) DBIM_ACTUAL : 니가 가장 원하는 사이즈는 얼마니? (가능하다면 이 사이즈를 유지하도록 해 줍니다.) DBIM_TITLE : 니 밴드의 제목이 뭐야? (유니코드로 대답해야 합니다.) DBIM_MODEFLAGS : 잡다한 기능중에 원하는거 있음 말해봐. (잡다한 기능 - DBIMF_NORMAL:됐어. DBIMF_VARIABLEHEIGHT:밴드 높이를 조정 가능하게 해줘.
DBIMF_DEBOSSED: 다른 밴드에 비해 움푹 들어간것 처럼 그려줘.
DBIMF_BKCOLOR: 배경색을 crBkgnd에 든 값으로 그려줘)
DBIM_BKCOLOR : 배경색깔 칠할까? (pdbi.dwMask := pdbi.dwMask and (not DBIM_BKCOLOR); 아냐 없애줘)
5. 마치자.
너무 지루한 내용이 되어 버렸네요. 이게 지금 동작은 하지만 문제가 되는 부분이 몇개 있습니다. 1. 검색어입력 Edit 컨트롤에 백스페이스가 안 먹힙니다. (T.T)
이건 왜 이런지 도통 알 수가 없어요. 분명 IInputObject와 관련이 있을것 같은데 자료가 너무 부족해서 포기했습니다. 아시는 분 꼭 좀 알려주세요.
2. 검색어입력 Edit 컨트롤에 포커스가 넘어올때 SellectAll 설정이 안됩니다.
이것 또한 원인을 찾지 못하고 있고 대책도 없습니다.
3. 쿨바위에서 오른쪽 버튼을 눌러 밴드감추기를 하면 팝업창이 한 번 더 튀어 나옵니다.
(델파이로 만들었다고 차별하는거야 뭐야!)
이런 고장난 부분들은 숙제로 남겨두어야만 하겠네요. 혹시라도 수리가 가능하면 다시 꼬리를 달겠습니다.
참고사이트 : http://msdn.microsoft.com/workshop/browser/ext/overview/Bands.asp
05 IE 확장자 - ToolBar Button
1. 툴버튼(Toolbar Button)이란?
지난 시간에는 CoolBar에 새로운 밴드를 추가 시키는 놈이었죠. 이번에는 기존의 툴밴드(뒤로/앞으로/중지/새로고침 버튼들이 있는 밴드)의 맨 뒤에 내가 만든 버튼을 집어 넣고 싶을때 만드는 확장자 입니다. 단추 하나만 있으면 되는데, 따로 밴드를 만들면 소잡는 칼로 닭잡는 격이 되지요. 보기에도 좋지 않구요. 일단 만들어 보자구요.
2. 들어가기 전에...
오늘 예제로 만들어볼 프로그램은 "상세정보"라는 버튼입니다. 그냥 이걸 누르면 현재 웹브라우저의 프레임별 간략 정보가 나옵니다. 원래는 제가 만든 IE스파이란 프로그램의 블랙 리스트 추가 버튼을 넣으려 했는데, 저게 무슨 프로그램인지 모르시는 분들은 이해하기 어려울 것 같아 그냥 암거나 대충 만들었습니다.
처음 시작부분은 지끔까지의 방식과 동일하고, 역시 컴공장을 사용합니다. 폼은 그 단추가 눌려졌을때 무슨일을 하느냐에따라 필요할 수도 있고 필요하지 않을 수도 있지만 기본적으로는 필요가 없습니다. 다만 단추를 그릴때 필요한 놈만 있으면 됩니다. 그 놈은 바로 "아이콘" 입니다. 프로그래머에게 항상 골치아픈 문제죠. 아이콘.. (나만 그런가?) 직접 그리자니 능력이 안되고 적당한 아이콘을 찾아 온 동네를 다 뒤지고 다녀도 맘에 쏙 드는 놈을 찾기가 힘들죠. IE에서는 버튼 하나에 4장의 그림이 필요합니다. (IE가 원하는건 6장이지만 2장은 사용되지 않고 있음)
16x16x16 흑백한장, 컬러한장 20x20x256 흑백한장, 컬러한장 (안쓰이는건 20x20x16)
16x16 아이콘은 전체화면모드(F11을 누르면 나오는)에서만 사용된다고 합니다.
제가 워낙 게을러 아이콘을 따로 준비하지 못하는 관계로 이 강좌에서는 그냥 IE에서 만들어 놓았지만 사용되고있지 않는 아이콘을 그냥 씁니다.
자세한 아이콘의 스펙은 http://msdn.microsoft.com/workshop/browser/ext/overview/toolbar_style.asp 을 참고 하세요.
툴버튼을 추가하는 방법은 모두 네 가지입니다. 1. COM 2. ExplorerBar 3. 스크립트파일 4. EXE 파일
물론 여기서 취급하는 것은 첫번째 COM 형식입니다.
3. 시작하자.
지금까지와 같은 방법으로 새 프로젝트를 열고 컴공장을 고칩니다. 컴공장은 지난 강좌와 똑같습니다.
const ButtonCaption = '상세정보'; SID_InfoButton = '{7988F6E0-502C-11D5-9C81-000102657B27}'; Class_TInfoButton: TGUID = SID_InfoButton;
...
procedure Install; var FN : array[0..MAX_PATH] of char; LibName : String; begin // 이 COM의 풀패스를 뽑아온다. SetString(LibName, FN, GetModuleFileName(hInstance, FN, SizeOf(FN)));
REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'Default Visible', 'Yes'); REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'ButtonText', ButtonCaption); // REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'HotIcon', ExtractFilePath(LibName)+'hot.ico'); // REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'Icon', ExtractFilePath(LibName)+'gray.ico'); REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'HotIcon', ',3'); REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'Icon', ',3'); // REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'HotIcon', LibName+',1'); // REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'Icon', LibName+',1'); REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'CLSID', '{1FBA04EE-3024-11d2-8F1F-0000F87ABD16}'); REG_SetString(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton, 'ClsidExtension', SID_InfoButton); end;
procedure UnInstall; begin REG_Delete(HKEY_LOCAL_MACHINE, '\Software\Microsoft\Internet Explorer\Extensions\'+SID_InfoButton); end;
initialization TComFac.Create(ComServer, TTInfoButton, Class_TInfoButton,
'TInfoButton', ,
Install, UnInstall,
ciMultiInstance, tmApartment);
뭐 별로 어려운건 없죠? 아이콘만 빼구요. 저두 궁금한게 아이콘은 분명 4장인데 레지스트리에 추가하는건 2개 뿐입니다. MSDN이 말하기로는 비주얼 스튜디오의 아이콘 만드는 툴을 사용한다는데, 그 놈이 아이콘 여러장을 하나의 ICO 파일에 뭉쳐 넣는 그런 기능을 가지고 있지 않을까 하는 짐작 정도만 되는군요. ICO 파일을 따로 만들어서 배포하는건 번거로운 일이죠. 보통은 리소스로 집어 넣고 씁니다. 이건 위의 리마크 라인을 참고하시구요. 지금 쓰인건 IE의 리소스를 사용하는 겁니다.
오늘 구현할 인터페이스는 비교적 간단합니다.
TTInfoButton = class(TComObject, IOleCommandTarget, IObjectWithSite) private
WebBrowser : IWebBrowser2;
protected {IOleCommandTarget}
function QueryStatus(CmdGroup: PGUID; cCmds: Cardinal;
prgCmds: POleCmd; CmdText: POleCmdText): HResult; stdcall;
function Exec(CmdGroup: PGUID; nCmdID, nCmdexecopt: DWORD;
const vaIn: OleVariant; var vaOut: OleVariant): HResult; stdcall;
{IObjectWithSite}
function SetSite(const pUnkSite: IUnknown ):HResult; stdcall; function GetSite(const riid: TIID; out site: IUnknown):HResult; stdcall;
end;
이 간단한 4개의 인터페이스에 엄청나고도 무지막지하고도 오묘하고도 복잡한 기능이 숨어 있습니다. 다만, 이 강좌에서는 최대한 단순하게 처리를 하겠습니다.
IObjectWithSite는 지난 강좌에서도 나왔었죠. 바로 웹브라우저의 포인터를 얻어오는데 이용됩니다. 이 버튼이 웹브라우저하고 상관 없이 돌아간다면.. (ex:버튼을 누르면 시스템을 종료해 버린다. --;) IObjectWithSite는 빼버려도 됩니다. IOleCommandTarget만 만들면 됩니다.
// 이건 초기화/마무리 시에 호출된다 그랬죠? function TTInfoButton.SetSite(const pUnkSite: IUnknown ):HResult; stdcall; begin Result := S_OK; WebBrowser := nil; if pUnkSite=nil then Exit;
Result := (pUnkSite as IServiceProvider).QueryService(IWebbrowserApp, IWebbrowser2, WebBrowser); end;
function TTInfoButton.GetSite(const riid: TIID; out site: IUnknown):HResult; stdcall; begin Result := E_NOINTERFACE; end;
이건 뭐 설명할 것두 없겠네요. SetSite에 파라미터로 IShellBrowser 인터페이스가 넘어온다는 것 정도.. 거기서 IWebBrowser2를 끌어 내는 것 뿐입니다.
IOleCommandTarget은 버튼과 관련된 인터페이스 입니다.
QueryStatus함수는 말그대로 버튼의 상태를 물어보는 부분이니까 내 버튼의 상태를 이야기 해주면 됩니다.
function TTInfoButton.QueryStatus(CmdGroup: PGUID; cCmds: Cardinal;
prgCmds: POleCmd; CmdText: POleCmdText): HResult; stdcall;
var I : Integer; begin if WebBrowser=nil then begin
Result := E_FAIL; Exit; end;
Result := S_OK; if cCmds<1 then Exit; For I:=0 to cCmds-1 do begin
prgCmds.cmdf := OLECMDF_ENABLED;
prgCmds := Pointer(DWORD(prgCmds)+Sizeof(Pointer));
end;
end;
prgCmds는 OLECMD 구조체의 배열입니다. 그 배열의 갯수는 cCmds가 되겠습니다. MSDN의 이 구조체 관련 도움말은 여기서 사용되는 것과 관계가 없으니 연관성을 찾으려 애쓰지 마세요. 거기 정의된 상수들은 모두 IE내부의 명령어들입니다. 그런 명령들이 지금 우리가 만드는 버튼으로 들어오지는 않습니다. OLECMD 구조체는
cmdID: Cardinal; // 물어보는 내용 cmdf : Longint; // 물음에 대한 응답을 넣는다.
이렇게 두 개의 항목만을 갖고 있습니다.
MSDN의 자세한 언급이 없어 도대체 내 버튼에게 뭘 물어보는지는 찾을 수가 없더군요. 하지만 설명이 없다고 모르겠습니까? 버튼에 와서 물어보는 거야 뻔하죠.. 지금 동작 가능하니? 안하니? 이것 밖에 더 있겠습니까? 배열로 된건 두 개 이상의 버튼을 지원하기 위함이 아닐까 하고 짐작할 따름입니다. 동작 가능하면 응답 값에 OLECMDF_ENABLED 를 세팅하면 됩니다.
const OLECMDF_SUPPORTED = $00000001; // The command is supported by this object. (무슨 말이야!) OLECMDF_ENABLED = $00000002; // 현재 버튼이 사용가능한 상태야. OLECMDF_LATCHED = $00000004; // 이 버튼은 토글 버튼인데 지금 눌려져 있는 상태야. OLECMDF_NINCHED = $00000008; // 뭐야! (나중에 쓸려고 예약된 기능입니다.) OLECMDF_INVISIBLE = $00000010; // 나머지 두개는 MSDN 공식상수에는 없고 델파이에 있는건데, 동작 여부는 알수 없음. 이름으로 짐작 바람. OLECMDF_DEFHIDEONCTXTMENU = $00000020;
리턴값은 위의 해당 상수들을 OR 시켜 대답하면 됩니다. (SHDocVW 유닛에 정의됨) 이 예제에서는 무조건 OLECMDF_ENABLED 이라고 대답합니다.
function TTInfoButton.Exec(CmdGroup: PGUID; nCmdID, nCmdexecopt: DWORD;
const vaIn: OleVariant; var vaOut: OleVariant): HResult; stdcall;
begin { if WebBrowser=nil then begin
Result := E_FAIL; Exit; end;
} // 상세정보폼을 만들어 보여준다. with TFormInfo.Create(nil) do begin
Prepare(WebBrowser);
ShowModal;
end;
Result := S_OK; end;
실제로 버튼이 눌릴때 호출되는 함수 입니다. MSDN 에서는 버튼이 클릭될때는 nCmdID=1, 메뉴에서 클릭될때는 nCmdID=2 가 넘어온다고 하는데.. --; 순전히 고진말입니다. 이 예제에서는 메뉴에 추가는 안되고 버튼만 있는데, 한 번을 누르든 백 번을 누르든 무조건 0만 넘어오더군요. 이 예제에서는 이것저것 따지지 않고 호출되면 그냥 실행시켜 버리도록 만들었습니다.
3. 끝났습니다.
이 예제는 이것저것 안따지고 정말 최대로 단순하게 만든겁니다. (그래도 잘 동작하네요.. ^^;) 모로 가도 서울만 가면 되니깐. 설명에서 언급하지 않은 여러 파라미터 들이 있죠? 궁금하신 분들은 직접 찾아보도록 하세요. 저는 머리가 나빠서..
참고사이트 : http://msdn.microsoft.com/workshop/browser/ext/tutorials/button.asp
06 - 확장자 만들기
1. 이번 강좌는..
마땅한 제목이 없어서 그냥 아무렇게나 붙였습니다. 오늘은 아무래도 강좌의 마지막이 될것 같아요. IE확장자나 쉘확장자들을 뒤져봐도 쓸데 없는 놈들만 남았네요. 쉘의 NameSpace가 좀 써먹을데가 있을것 같긴하지만, T.T 넘 어려워서 제가 감당을 못하겠어요. 담에라도 깨우치게 되면 올리도록 할께요. (탐색기에 가상드라이브, 가상디렉토리를 만드는 기능을 가지고 있습니다.)
그래서 마지막 강좌로 마땅히 할만한게 없어, 내가 만든 프로그램에 확장자 넣기를 다뤄 보도록 하겠습니다. 확장자를 사용자가 추가할 수 있는 프로그램.. 괜히 뭔가가 특별해 보이지 않으세요? 스킨따위를 사용자가 추가하는거와는 질적으로 다르죠. 물론 이걸 이용하면 스킨도 만들수 있습니다. dll로 배포되는 스킨 --; 하지만 확장자의 장점은 이름에도 알수 있듯이 아무래도 확장성이 아니겠습니까? 나중에 기능을 추가 한다든가 하는 길을 미리 열어두는거죠.
통신 프로그램을 하나 만든다고 쳐요. zmodem은 내장을 시켰는데, 기타 x,y,kermit 등의 구식 프로토콜도 내장시키자니 공부하기 귀찮고 시간도 촉박한 상황이다. 이럴때 파일 다운로드 인터페이스를 하나 만들어 길을 열어놓고 그냥 배포하는 겁니다. 그 후 잘 팔린다거나, 프로토콜이 왜 zmodem뿐이냐는 항의가 엄청 나게 들어오면 다른 프로토콜을 확장자로 만들어 하나씩 던져 주는거죠 ^^; 만들기가 귀찮으면 그냥 그 인터페이스만 공개해도 됩니다. 그러면 답답한 사람이 직접 만들어서 올릴테니까요. --;
오늘 만들어볼 프로그램은 두가지 입니다. 확장 기능을 지원하는 일반 응용프로그램(EXE), 그리고 그놈의 확장자(DLL).. 강좌에서는 간단하게 버튼을 추가하는 것만 취급하지만, 어떤놈을 확장하더라도 다 마찬가지기 때문에 응용은 어렵지 않을겁니다.
2. 인터페이스
unit UnitInterface;
interface
const SID_MyButton = '{D4DEF9C0-569B-11D5-9C81-000102657B27}'; IID_MyButton : TGUID = SID_MyButton;
AC_CAPTION = 1; AC_ICON = 2;
type IMyButton = Interface (IUnknown) [SID_MyButton] function OnCreate(const Memo : TObject) : HResult; SafeCall; function OnClick : HResult; SafeCall; function OnClose : HResult; SafeCall; function AskInfo(const AskCode : LongInt; var Reply : WideString) : HResult; SafeCall; end;
implementation
end.
인터페이스에는 최소의 기능만을 넣었습니다. 델파이의 이벤트 이름과 똑같은 이름을 사용했으니 알아보기도 쉬울겁니다.
- OnCreate
확장자 초기화시에 불려지는 함수. 파라미터로 TMemo를 넣어서 호출한다. 여기서 잠깐.. 아시다시피 TMemo는 델파이에서만 쓰는 컴퍼넌트입니다. 따라서 이런 식으로 만들게 되면 VC++ 쓰는 사람들은 절대 이놈의 확장자를 만들 수 없습니다. (Builder는 가능할지도..) 그말은, 이런식으로 만들면 안됀다는 그런 말입니다. 하지만 인터페이스를 공개할게 아니고 앞으로도 내가 계속 확장자를 만들거라면 이렇게 해도 됩니다. 델파이로 만들면 되니깐. 암튼 여기서는 이렇게 합니다 ^^;
- AskInfo
응용프로그램이 확장자에게 "캡션", "아이콘" 등을 물어보는 용도로 만들었습니다. 이렇게 해도 되고 그냥 레지스트리에 기록한걸 받아도 되고 뭐.. 사용하기 나름이죠.
3. 응용프로그램
// 폼이 생성될때 초기화하는 부분입니다. // 확장자들을 여기서 다 등록을 하자구요. 물론 확장자에따라 다르게 처리할 수도 있음. procedure TFormMain.FormCreate(Sender: TObject); const ExtensionLocation = 'Software\JazzTest\Extension'; // 확장자가 기록되는 레지스트리입니다. var COM_SID : String; COM_CLSID : TGUID; COM : IUnknown; REG : TRegistry; NameBuf : TStrings; I : Integer; begin Memo.Align := alClient;
REG := TRegistry.Create; REG.RootKey:=HKEY_LOCAL_MACHINE;
// 등록된 확장자가 있는지 살펴 봅니다. 없으면 그만이고.. if not REG.OpenKey(ExtensionLocation, False) then Exit; if not REG.HasSubKeys then Exit;
// 등록된 확장자의 키값들을 TStrings 버퍼에 담아서.. NameBuf := TStringList.Create; REG.GetKeyNames(NameBuf); REG.CloseKey;
// 하나하나 살펴봅니다. for I:=0 to NameBuf.Count-1 do begin if not REG.OpenKey(ExtensionLocation+'\'+NameBuf.Strings[I], False) then Continue; COM_SID := REG.ReadString('CLSID'); try COM_CLSID := StringToGUID(COM_SID); except COM_SID := ; end; if COM_SID= then Continue;
// 확장자라고 등록된 놈들을 일단 실행시켜 봅니다. COM := CreateComObject(COM_CLSID); if not Assigned(COM) then Continue; // 실행이 되면 무슨 확장자인지 검사하러 보냅니다. CheckIN(COM); end; end;
// 무슨확장자인지 검사를 합니다.
function TFormMain.CheckIN(const COM: IUnknown): Boolean;
var
Buf : IUnknown;
I : Integer;
begin
Result := False;
if SUCCEEDED(COM.QueryInterface(IID_MyButton, Buf)) then begin // 버튼 확장자인가?
I := Length(FComButton); // FComButton 은 전역 동적배열 입니다.
SetLength(FComButton, I+1); // 버튼 확장자들을 여기에 넣어서 보관합니다.
FComButton[I] := Buf as IMyButton;
ComButtonCreate(Buf as IMyButton, I); // 버튼을 만들라고 보냅니다.
Result := True;
end;
end;
// 여기서 버튼을 만들어주게 됩니다.
// 그냥 ToolBar를 통째로 넘겨주고 확장자한테 니가 알아서 만들라고 할 수도 있지만..
// 그러면 못된 사람이 기존의 버튼들을 빼버릴 우려가 있으니깐..
procedure TFormMain.ComButtonCreate(const COM: IMyButton; const Index : Integer);
var
TB : TToolButton;
Buf : WideString;
begin
TB := TToolButton.Create(Self);
TB.Tag := Index; // 버튼 확장자가 여러 개 일 수도 있으니 Tag로 구분 합니다.
TB.OnClick := MyButtonClick; // 이벤트를 묶어 주고..
if SUCCEEDED(COM.AskInfo(AC_CAPTION, Buf)) then TB.Caption := String(Buf);
// 버튼의 캡션을 물어봅니다. 이 예제에서 아이콘은 지원 안함.
ToolBar.InsertControl(TB); // 툴바 맨뒤에 버튼을 추가. TB.Left := 100000; COM.OnCreate(Memo); // 인터페이스 함수를 불러줍니다. 파라미터로 Memo를 넘김. end;
// 버튼이 눌리면 그냥 확장자의 OnClick을 부르는것 뿐입니다.
procedure TFormMain.MyButtonClick(Sender: TObject);
begin
FComButton[TToolButton(Sender).Tag].OnClick;
end;
// 프로그램 종료시에 각 확장자들의 OnClose를 호출 procedure TFormMain.FormDestroy(Sender: TObject); var I : Integer; begin if Length(FComButton)>0 then For I:=0 to Length(FComButton)-1 do FComButton[I].OnClose; end;
이상의 프로그램이 첨부된 APP 폴더에 들어 있습니다.
일단 컴파일하고 실행시켜 보시기 바랍니다. 위에 버튼들이 4개 있죠. 분명히 4개입니다. 옆에다가 4개라고 적어 놓으세요. 나중에 뭐가 달라졌냐고 묻지 마시고..
이 프로그램을 배포해서 선풍적인 인기를 끌었습니다. (--;) 그리고는 3달 후 사용자들이 메모창의 내용을 메세지 박스에서 볼 수 있게 달라고 수 천통의 항의 메일을 보냅니다. (--; __;)
그래서 프로그래머는 다음의 확장자를 만들어 배포하게 됩니다.
4. 확장자
컴공장에서 레지스트리에 등록하는건 설명에서 생략하기로 하겠습니다. 이건 IMyButton 인터페이스를 구현합니다.
TMyButtonTest = class(TComObject, IMyButton) private RemoteMemo : TMemo; protected {IMyButton} function OnCreate(const Memo : TObject) : HResult; SafeCall; function OnClick : HResult; SafeCall; function OnClose : HResult; SafeCall; function AskInfo(const AskCode : LongInt; var Reply : WideString) : HResult; SafeCall; end;
여기서 잠깐.. 클래스만 만들어 놓고 shift+ctrl+C 를 힘차게 한번 눌러 보세요. implementation 영역에 자동으로 코딩 할수 있게 함수들이 쭈루룩 배치가 됩니다. 와하하.. 정말 신기하죠! 왜 진작 가르쳐 주지 않았냐구요? 저도 오늘 알았습니다. --; 박후선님이 유명하시다길래 그분 강좌를 뒤져보다가 발견한겁니다. 이런건 구석 강좌말고 공지사항에다가 올려야 하는 팁인데 말이죠.
// 프로그램 초기화때 Memo 컴포넌트를 담고 호출된다.
// 이 Memo는 내 Memo처럼 떡주무르듯 다룰 수 있다.
// 이 메모를 RemoteMemo라는 전역 변수에 잘 보관한다.
function TMyButtonTest.OnCreate(const Memo: TObject): HResult;
begin
RemoteMemo := TMemo(Memo);
Result := S_OK;
end;
// 응용프로그램이 여기에 와서 물어본다. 캡션만...
function TMyButtonTest.AskInfo(const AskCode: Integer;
var Reply: WideString): HResult;
begin
Result := S_OK;
Reply := ;
case AskCode of
AC_CAPTION : Reply := '컴버튼';
AC_ICON : Reply := '1';
end;
if Reply= then Result := S_FALSE;
end;
// 버튼이 클릭되면 호출된다. // 메모의 내용을 메세지창에 띄우는 예제.. function TMyButtonTest.OnClick: HResult; begin if Assigned(RemoteMemo) then ShowMessage(RemoteMemo.Text); Result := S_OK; end;
// 이건.. 프로그램 종료할때 호출된다. function TMyButtonTest.OnClose: HResult; begin Result := S_OK; end;
이제 확장자도 완성입니다. (첨부의 COM 폴더) 레지스트리에 지금 만든 확장자를 등록 합니다. 그리고는 탐색기로 가서 3달전에 만들어 두었던 APP 폴더의 APP.EXE를 실행시켜 보세요.
아까와는 달리 젤뒤에 쪼금 큰 버튼이 생겼죠? 눌러보세요...
5. 마치면서..
강좌를 시작할때는 아주 길고도 오래 걸릴거라고 예상을 했었는데... --; 몇주만에 끝나버리는 군요. 내가 아는 지식이 단지 몇주 밖에 못버티다니.. T.T 암튼 단 한분에게라도 도움이 되었다면 저로써는 그만입니다.
수고들 하세요~
