From YYpBD's MediaWiki
------------------------------------------------------------------------
* 이 글의 저작권은 저자에게 있습니다.
* 글의 출처와 내역을 밝힐 경우... 어디에든 게재하실 수 있습니다.
그러나, 상업적인 사용은 금합니다.
델파이의 모든 것. 델파이 코리아. http://www.delphikorea.com
------------------------------------------------------------------------
*** 메시지 갈구리질을 이용한 32비트 서브클래싱.
좀 버그가 가라앉은 다음에 정리하려고 했는데, 영재님의 협박에 못이겨
일단 되는 곳 까지만 함께 공유해 보도록 하겠습니다. 참고자료가 많지 않았고
따라서 저도 제가 제대로 사용하는건지 알 수 없는 완벽한 흉기수준이기 때문에
따라하시는 동안에 무수한 퍼런화면을 구경하시게 될 껍니다~ ^^;
항상 그렇지만, 쓰는 넘 맘대로 존칭은 생략합니다.
* 들어가며.
32비트 환경에서는 기본적으로 다른 어플리케이션의 메시지 처리에 손댈 수
없다. 일년쯤 전에 훅 강좌를 통해 메시지 갈구리질을 이용하면 윈사이트같은
어플이 가능한 것 처럼 헷소리를 했었는데... 사실 어림도 없는 이야기다.
갈구리질을 통해서는 기껏 키보드, 마우스, 윈도 관련 메시지 몇 개 정도를 만지작
거리는 것이 고작이다. 내 어플이 아닌 다른 어플에 WM_USER+1818 같은 희한한
메시지가 날아온다면? 갈구리질로 이 것을 감지하는 방법은 전혀 없다. 사용자
정의 메시지가 아닌 일반 메시지 역시 해당 어플리케이션에서 처리하지 않는다면
훅 체인에도 걸리지 않는다.
이런 이유들 때문에 좀 더 뽀다구 나고 능동적인 어플을 만들기 위해서는 다른
어플리케이션의 윈도 프로시저를 갈아치워 메시지에 대한 반응을 내 맘대로
뜯어고쳐야 할 필요가 생기게 된다. 이 강좌에서는 지난번에 구현했던 자석폼
효과를 시스템 전체에 확대하는 방법을 통해 메시지 갈구리질을 이용한 32비트
서브클래싱의 원리와 구현방법에 대해 알아볼 것이다. 기본적으로 훅킹을 자유롭게
쓸 수 있는 사람을 대상으로 했기 때문에 먼저 오래전에 올렸던 메시지 갈구리질
강좌와 메모리 맵 강좌를 자신의 것으로 하기 바란다.
* 같은 프로세스 내에서의 서브클래싱.
서브클래싱이란 특정 컨트롤로 날아오는 메시지를 내 마음대로 조작하기 위한
기법이다. 윈도 컨트롤은 저마다 메시지를 처리하기 위한 “윈도 프로시저”를
가지고 있으며 GetWindowLong() API 에 GWL_WNDPROC 인자를 주어 호출함으로써
해당 윈도 프로시저의 주소를 얻어낼 수 있다. 예제를 통해 서브클래싱에 대해
간단히 이해해 보도록 하자.
var
OldWinProc: Integer;
function NewWinProc(hWnd: HWND; Msg: WORD; wParam: WORD; lParam: LONGINT):
LONGINT; StdCall;
begin
case Msg of
WM_USER+1818 : ShowMessage('1818 메시지 날아왔어~');
end;
Result := CallWindowProc(Pointer(OldWinProc), hWnd, Msg, wParam, lParam)
end;
빈 폼에 버튼을 하나 올려두고 폼의 OnCreate 이벤트와 OnDestroy 이벤트에서
위에서 정의한 윈도 프로시저를 설정하고 해제하자.
procedure TForm1.OnCreate(Sender: TObject);
begin
OldWinProc := SetWindowLong(Button1.Handle, GWL_WNDPROC,
LongInt(@NewWinProc));
end;
procedure TForm1.OnDestroy(Sender: TObject);
begin
SetWindowLong(Button1.Handle, GWL_WNDPROC, OldWinProc);
end;
Button1으로 전달되는 메시지는 먼저 NewWinProc를 통과한다. 만약 이 메시지가
WM_USER+1818 이라면 버튼은 메시지 폼을 보여주고 원래의 윈도 프로시저를 호출할
것이다. 실제 메시지를 날려 정확히 동작하는지 확인해 보자.
procedure TForm1.OnSpeedButton1Click(Sender: TObject);
begin
SendMessage(Button1.Handle, WM_USER+1818, 0, 0);
end;
델파이 내에서는 핸들이 없는 그래픽 컨트롤에도 서브클래싱을 걸 수 있는데,
델파이는 윈도의 메시징 시스템과는 별개의 가상적인 메시징 시스템을 통해 핸들이
있는 부모 컨트롤로 전달되는 메시지를 각 컨트롤의 Perform 메소드를 통해
강제적으로 전달하고 있기 때문이다. 델파이의 각 컨트롤들은 WndProc라는
프로퍼티를 가지고 있으며 이것을 바꿔침으로써 해당 컨트롤들의 메시지 처리를
마음대로 주무를 수 있다.
type
TForm1 = class(TForm)
Label1: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
LabelOrgWndProc : TWndMethod;
procedure LabelWndProc( var Msg : TMessage );
……
procedure TForm1.LabelWndProc( var Msg : TMessage );
begin
case Msg.Msg of
CM_MOUSELEAVE:
begin
Label1.Caption := '나갔다';
Label1.Font.Color := clBlack;
end;
CM_MOUSEENTER:
begin
Label1.Caption := '들어왔다';
Label1.Font.Color := clBlue;
end;
end;
LabelOrgWndProc(Msg);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
LabelOrgWndProc := Label1.WindowProc;
Label1.WindowProc := LabelWndProc;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Label1.WindowProc := LabelOrgWndProc;
end;
위의 소스에서 조금 이상한 점을 느꼈을 것이다. 제일 처음에 예제로 사용한
버튼의 서브클래싱에서는 전역 함수를 썼는데 그럼 이 전역함수가 어떻게 각
클래스의 내부 함수로 융화될 수 있었을까… 재미있게도 VCL은 윈도 프로시저의
인스턴스를 생성하는 MakeObjectInstance라는 함수를 사용해 생성된 각각의
클래스마다 서로 다른 윈도 프로시저를 정의할 수 있도록 구성되어 있다. 다음은
처음에 시도했던 버튼에 대한 서브 클래싱을 Form1 내부에 선언된 윈도 프로시저
함수를 통해 구현해 본 것이다.
private
FOldProc: Pointer;
FNewProc: Pointer;
procedure NewWndProc( var Message: TMessage );
……
procedure TForm1.NewWndProc(var Message: TMessage );
begin
case Message.Msg of
WM_USER+1818 : ShowMessage('1818 메시지 날아왔어~');
end;
with Message do
Result := CallWindowProc( FOldProc, Button1.Handle, Msg, WParam, LParam );
end;
procedure TForm1.OnCreate(Sender: TObject);
begin
FOldProc := Pointer(GetWindowLong(Button1.Handle, GWL_WNDPROC));
FNewProc := MakeObjectInstance(NewWndProc);
SetWindowLong(Button1.Handle, GWL_WNDPROC, LongInt(FNewProc));
end;
procedure TForm1.OnDestroy(Sender: TObject);
begin
SetWindowLong(Button1.Handle, GWL_WNDPROC, LongInt(FOldProc));
FreeObjectInstance (FNewProc);
end;
이것은 전역 서브클래싱을 시도할 때 매우 중요한 개념이므로 반드시
숙지하도록 하자.
(여담이지만 훅 프로시저 같은 기타 콜백함수들 역시 위와 같은 메커니즘을
사용하면 각 클래스별로 독립시켜 동작하게 할 수 있겠지만, 아직 필자의 능력이
부족해 성공하지 못했습니다... ^^)
****************************************
흐~ 다음편에서는 32비트 서브클래싱이 가능한 원리에 대해 설명하고 실제로
다른 윈도를 괴롭혀 보도록 하겠습니다. ~~ 우히히~~ ^^;
---------------------------------------------------------------------
* 이 글의 저작권은 저자에게 있습니다.
* 글의 출처와 내역을 밝힐 경우... 어디에든 게재하실 수 있습니다.
그러나, 상업적인 사용은 금합니다.
출처 : 델파이 코리아. http://www.delphikorea.com
---------------------------------------------------------------------
2편이 많이 늦었습니다. 1편 강좌를 시작했을 때 마소 연재를 제의받았고,
기사의 내용과 흐름을 맞추기 위해 부득이하게 뒷편을 이제야 올립니다...
^^;
**전역 훅의 또 다른 의미.
프로그래머의 관점에서 훅의 유효범위란 부분을 되짚어 보자.
훅은 미리 정의된 콜백함수 (Callback function) 이며 시스템에서는
이미 정의되어있는 프로토타입에 근거해 이것을 호출하게 된다. 일단
로컬훅에서는 어플리케이션 내에서 발생하는 이벤트를 잡아챌 수 있고,
이것을 전역 훅으로 확대하면 설정된 이벤트가 어떤 어플리케이션에서
발생하든지 그 사실을 알아낼 수 있게 된다. 이 말의 의미는 "윈도
95 이상에서 구동하는 어플리케이션은 각각 독립된 주소공간에서
돌아가고 서로의 프로세스를 침범할 수 없다." 고 말하는 마이크로
소프트사의 공식적 입장을 완전히 뒤집는 것이 되는데, 다른
프로세스에서 실행될 코드를 우리가 만드는 어플리케이션에서 정의할
수 있다는 얘기와 같기 때문이다.
좀 더 간단하게 생각해 보자. 32비트 환경에서는 각 프로세스마다
데이터 영역은 ‘보호’되지만 DLL을 통해 코드영역은 ‘공유’되고
있다. 만약 전역 훅 프로시저가 들어있는 DLL에 또 다른 함수를
정의하고 이 함수를 훅 프로시저에서 호출한다면 어떻게 될까? 해당
어플리케이션은 훅 프로시저를 실행하기 위해 이미 훅 DLL을 불러들인
상태이며 때문에 함께 포함된 함수도 원하건 원치 않건 덩달아
호출하게 된다. 우리가 원하는 함수를 싫건 좋건 훅 프로시저를
수행하면서 함께 실행한다는 이야기다. 이처럼 DLL을 통해 공유되고
있는 전역 훅 프로시저를 이용하면 프로세스간의 경계를 무너뜨리고
필요한 코드를 다른 프로세스의 주소공간 속으로 밀어넣을 수 있고
당연히 다른 어플리케이션에 대한 서브클래싱도 가능하게 된다.
재미있게도 이것은 9X/NT/2000 등 각 32비트 윈도의 버전을 가리지
않고 윈도 프로시저를 바꿔치기 할 수 있는 유일한 방법이기도 하다.
참고로 다음 리스트는 캡션창 등 윈도의 NC영역에서 마우스가 눌릴
때, DLL을 호출한 어플리케이션의 실행파일명을 클립보드에 적어주는
전역 마우스훅 DLL의 일부이다. 이 예제를 통해 훅 프로시저가
실행되는 시점이 정말로 각 어플리케이션 프로세스의 내부라는
사실을 확인할 수 있다.
*호출한 어플리케이션을 확인하는 마우스 훅 프로시저.
......
var
HMouseHook : HHook;
procedure ReadData;
var
F : TFileStream;
begin
if not FileExists(HookDataFile) then Exit;
F := TFileStream.Create(HookDataFile, fmOpenRead);
try
F.Read(HMouseHook, sizeof(HMouseHook));
finally
F.Free;
end;
end;
procedure WriteData;
var
F : TFileStream;
begin
F := TFileStream.Create(HookDataFile, fmCreate);
try
F.Write(HMouseHook, sizeof(HMouseHook));
finally
F.Free;
end;
end;
function GetExeName : String;
begin
setLength(Result, Max_Path);
GetModuleFileName(0, PChar(Result), Max_Path);
SetLength(Result, StrLen(PChar(Result)));
end;
function MouseClickHookProc(Code: Integer; wParam: WPARAM;
lParam: LPARAM) : LongInt; stdcall;
begin
if Code >= 0 then
if wParam = WM_NCLBUTTONDOWN then
ClipBoard.AsText := GetExeName;
ReadData; {파일에 저장된 마우스 훅의 핸들을 읽어온다.}
Result :=
CallNextHookEx(HmouseHook, Code, wParam, lParam);
end;
......
**실전 1. 바탕화면에 그림 그리기.
물론 여기서 알아볼 바탕화면의 그림은 등록정보에서 설정하는
배경그림과는 완전히 다른 얘기이다. 사실 글쓴 놈이 32비트
서브클래싱에 관심을 가지게 된 결정적인 이유이기도 하다.
이 기법을 이용하면 많은 아이디어를 현실화 하는 것이 가능해
진다. 광고를 보여주고 사이버 머니를 적립하는 서비스를 생각해
보자. 일반적으로 사용되는 ‘뷰바’라는 이름의 광고창들의
경우 이 또한 결국 윈도이기 때문에 핸들만 얻어서 ShowWindow()나
MoveWindow() 같은 API를 호출하면 얼마든지 사용자의 눈에 띄지
않는 곳으로 옮기거나 감춰버릴 수 있다. 서비스를 제공하는
입장에서는 많이 억울하지 않겠는가? 물론 이것을 막기위해
여러가지 방법들이 사용되고 있지만, 아예 이 광고를 바탕화면에
뿌려버린다면 어떨까? 또 다른 예로 바탕화면에 항상 보이면서
컴퓨터 사용을 방해하지 않는 투명달력 같은 곳에 응용해도
재미있을 것이다.
먼저 바탕화면 윈도의 핸들을 구해보자. 윈사이트를 뚫어지게
쳐다보면 바탕화면 윈도의 위치를 얻을 수 있는데. 'Progman'
이라는 클래스명을 가진 윈도의 자식으로 'SHELLDLL_DefView'
란 윈도가 있고 그 밑에 'SysListView32'라는 윈도가 바로
우리가 원하는 바탕화면의 윈도이다. 사용자가 액티브 데스크탑을
켰을 때는 'Internet Explorer_Server'란 윈도가 바탕화면이
되지만 주제에서 벗어나는 얘기이므로 여기서는 다루지 않겠다.
특정 윈도에 속한 자식 윈도의 핸들을 얻어내는 방법은
여러가지가 있다. 글쓴넘은 경우에는 EnumChildWindows()
API를 주로 이용하는데, 다음 리스트는 바탕화면 윈도의
핸들을 얻는 프로시저를 보여주고 있다.
*바탕화면 윈도의 핸들 얻기.
var
DTHwnd : THandle;
procedure SetDTHWnd;
function GetClassText(Hwnd : THandle) : String;
begin
setLength(Result, Max_Path);
GetClassName(Hwnd, PChar(Result), Max_Path);
SetLength(Result, StrLen(PChar(Result)));
end;
function EnumChildWindowsProc(Hwnd : THandle;
var lParam : DWord) : boolean; stdcall;
begin
Result := True;
if GetClassText(Hwnd) = 'SysListView32' then
begin
DTHwnd := Hwnd;
Result := false;
end;
end;
begin
DTHwnd := 0;
EnumChildWindows(FindWindow('ProgMan', 'Program Manager'),
@EnumChildWindowsProc, 0);
end;
이번에는 서브클래싱을 걸어주고 해제하는 간단한 객체를 구현해
보자. 윈도의 배경이 갱신될 때 발생하는 WM_ERASEBKGND 메시지의
wParam에는 그림이 그려질 DC의 정보를 들어 있고, 바탕화면
윈도우는 이 메시지를 받을 때 배경그림이나 지정된 배경색을
채워주고, 다시 WM_PAINT 메시지가 발생할 때 아이콘들을 그려주게
된다. 때문에 WM_ERASEBKGND 메시지가 발생할 때 미리 리소싱해
둔 'PICTURE1'이라는 비트맵 리소스를 불러와 해당 DC에 뿌려주면
그림은 아이콘과 배경 사이에 놓이게 되는 것이다. 이맘때 쯤에서
도대체 이런 과정들을 글쓴 넘이 어떻게 알아냈는지 궁금할 것이다.
이런 결론을 얻기 위해 윈사이트를 뚫어져라 들여다보고 몇가지
가설을 세운 다음 각각의 테스트용 프로그램들을 만들어 확인하는
과정을 여러 번 반복했었다. 한가지 귀뜸하면, 메시지의 흐름을
손에 익히기 위해서는 윈사이트와 같은 스파이 프로그램들과
아주아주 친해져야 한다.
*서브클래싱용 객체.
type
TSubClassObj = class(TObject)
private
FTargetWnd : THandle;
FOldDTProc: Pointer;
FNewDTProc: Pointer;
procedure NewDTWndProc(var Message: TMessage);
public
constructor Create(Wnd : THandle);
destructor Destroy; override;
end;
{ TSubClassObj }
// 설치할 새로운 윈도 프로시저.
procedure TSubClassObj.NewDTWndProc(var Message: TMessage);
var
ACanvas : TCanvas;
ABitmap : TBitmap;
begin
with Message do
Result :=
CallWindowProc(FOldDTProc, FTargetWnd, Msg,
WParam, LParam);
Case Message.Msg of
WM_ERASEBKGND : // WM_ERASEBKGND 메시지가 발생할 때...
begin
ACanvas := TCanvas.Create;
ABitmap := TBitmap.Create;
try
ACanvas.Handle := Message.wParam;
ABitmap.Handle := LoadBitmap( HInstance, 'PICTURE1');
ACanvas.Draw(100, 100, ABitmap);
finally
ACanvas.Free;
ABitmap.Free;
end;
end;
end;
end;
// 서브클래싱 걸기.
constructor TSubClassObj.Create(Wnd: THandle);
begin
FTargetWnd := Wnd;
FOldDTProc := Pointer(GetWindowLong(Wnd, GWL_WNDPROC));
FNewDTProc := MakeObjectInstance(NewDTWndProc);
SetWindowLong(Wnd, GWL_WNDPROC, LongInt(FNewDTProc));
end;
// 서브클래싱 해제.
destructor TSubClassObj.Destroy;
begin
SetWindowLong(FTargetWnd, GWL_WNDPROC,
LongInt(FOldDTProc));
FreeObjectInstance (FNewDTProc);
inherited;
end;
이제 이렇게 정의한 서브클래싱 코드를 다른 프로세스로 보내기
위해 적절한 훅 프로시저를 설정하는 문제가 남아 있다. 특별히
정해진 '룰'이 없기 때문에 실제 적용할 때 많은 시행착오와
경험을 필요로 하는 부분이 아닐까 싶다. 바탕화면에 그림넣기의
경우 이런 저런 방법을 고민하다가 어플리케이션의 실행과 동시에
서브클래싱을 구동하는 편이 좋을 것 같아 CBT훅을 선택했다.
CBT훅은 윈도의 생성, 소멸, 이동, 크기조절 등 대부분의 동작에
반응하며 무엇보다 WM_SYSCOMMAND메시지를 감지하는 기능을
가지고 있어 좋은 '뇌관'으로 써먹을 수 있다.
클라이언트 영역을 마우스로 이동하는 경우 등에 때때로
사용했던 WM_SYSCOMMAND의 '메직키'는 사실 해당 윈도를 구성하는
시스템 메뉴 리소스의 아이디를 뜻한다. 아직 여기엔 빈 영역이
많고 따라서 잘 사용되지 않을듯한 숫자를 선정해 서브클래싱을
설치하고 해제하는 신호로 삼아보았다. 다음 리스트는
WM_SYSCOMMAND메시지가 날아온 경우 wParam이 $FFF0이고 lParam의
값이 $8081이면 서브클래싱 객체를 생성하고, $8080이면 해제하는
CBT훅 프로시저를 나타내고 있다. RedrawWindow() API는
서브클래싱을 설치한 후 바로 그림이 그려지는 것을 볼 수 있도록
화면을 갱신하기 위해 쓰였고 해제할 때 역시 마찬가지 이유로
들어가 있다.
*서브클래싱을 걸어주는 CBT훅 프로시저.
function CBTHookProc(Code : Integer; wParam: WPARAM;
lParam: LPARAM) : LongInt; stdcall;
begin
if Code = HCBT_SYSCOMMAND then
begin
if wParam = $FFF0 then
begin
Case lParam of
$8080 :
begin
SubClassObj.Free;
SubClassObj := nil;
RedrawWindow(0, nil, 0, RDW_ERASE or
RDW_INVALIDATE or RDW_ALLCHILDREN);
end;
$8081 :
if not Assigned(SubClassObj) then
begin
SetDTHWnd;
SubClassObj := TSubClassObj.Create(DTHwnd);
RedrawWindow(0, nil, 0, RDW_ERASE or
RDW_INVALIDATE or RDW_ALLCHILDREN);
end;
end;
Result := Integer(True);
Exit;
end;
end;
ReadData; {파일에 저장된 CBT 훅의 아이디를 읽어오는 프로시저.}
Result := CallNexthookEx(HCBTHook, Code, wParam, lParam);
end;
애석하게도 CBT 훅에서 HCBT_SYSCOMMAND 코드를 참조할 경우는 훅
프로시저가 호출된 핸들이나 쓰레드의 정보가 넘어오지 않기 때문에
훅을 설치하는 단계에서 해당 쓰레드에서만 동작하도록 해야 한다.
다음 리스트는 GetWindowThreadProcessID() API를 이용해 바탕화면
윈도에 해당하는 쓰레드를 얻고 다시 여기에 WM_SYSCOMMAND 메시지를
보내 서브클래스 코드를 동작시키는 예를 보여주고 있다.
*CBT훅의 설치 및 서브클래싱 시작.
function StartDTDraw : Boolean;
begin
// 바탕화면 윈도를 구하고...
SetDTHwnd;
// 구한 바탕화면 핸들의 쓰레드에 CBT훅 설정하기.
Result := Boolean(SetWindowsHookEx(
WH_CBT, CBTHookProc, HInstance,
GetWindowThreadProcessID(DTHWnd, nil)
));
// 서브클래싱을 위해 방아쇠를 당긴다.
SendMessage(DTHWnd, WM_SYSCOMMAND, $FFF0, $8081);
// 훅 아이디를 파일에 적어준다.
WriteData;
end;
**실전 2. 시스템 전체에 자석효과 주기.
이번엔 오래전에 만들었던 자석효과를 시스템 전체로 확대해 보자.
(자석효과의 원리에 대해서는 "자석효과를 만들어 행복하게 살아보세~"
라는 강좌를 참고하세요~^^) 자석효과를 적용해야 할 시점은 사용자가
마우스로 윈도의 캡션부분을 잡아 움직이는 때이므로 마우스 훅을
'뇌관'으로 쉽게 떠올릴 수 있을 것이다. NC영역에서 마우스가 눌릴
때 각 윈도의 WM_MOVING 메시지를 잡아채는 서브클래싱을 걸어주고
다시 마우스가 떨어질 때 해제해 준다면 시스템의 모든 윈도에
'자석효과'를 줄 수 있게 된다.
기본적인 골격은 위에서 살펴본 바탕화면에 그림넣는 예제와
비슷하다. 다음 리스트는 WM_MOVING메시지를 처리하는 서브클래싱용
객체를 보여주고 있다.
*자석효과용 서브클래싱 객체.
type
TSubClassObj = class(TObject)
private
FStartPos : TPoint;
FDTArea : TRect;
FTargetWnd : THandle;
FOldProc: Pointer;
FNewProc: Pointer;
procedure NewWndProc(var Message: TMessage);
public
constructor Create(Wnd : THandle);
destructor Destroy; override;
end;
{ TSubClassObj }
constructor TSubClassObj.Create(Wnd: THandle);
var
SelfRect : TRect;
begin
FTargetWnd := Wnd;
// 자석효과를 위한 참조 변수들 셋팅.
FStartPos := Mouse.CursorPos;
GetWindowRect(Wnd, SelfRect);
FStartPos.X := FStartPos.X - SelfRect.Left;
FStartPos.Y := FStartPos.Y - SelfRect.Top;
SystemParametersInfo(SPI_GETWORKAREA, 0, @FDTArea, 0);
// 서브클래싱을 건다.
FOldProc := Pointer(GetWindowLong(FTargetWnd, GWL_WNDPROC));
FNewProc := MakeObjectInstance(NewWndProc);
SetWindowLong(FTargetWnd, GWL_WNDPROC, LongInt(FNewProc));
end;
destructor TSubClassObj.Destroy;
begin
inherited;
// 서브 클래싱 해제.
SetWindowLong(FTargetWnd, GWL_WNDPROC, LongInt(FOldProc));
FreeObjectInstance (FNewProc);
end;
procedure TSubClassObj.NewWndProc(var Message: TMessage);
var
SelfRect : TRect;
begin
with Message do
Result :=
CallWindowProc( FOldProc, FTargetWnd, Msg, WParam, LParam );
Case Message.Msg of
WM_MOVING :
with TRect(Pointer(Message.lParam)^) do
begin
// 서브클래싱중인 윈도의 크기를 얻는다.
GetWindowRect(FTargetWnd, SelfRect);
// 각 테두리에 붙이기.
if (Left < FDTArea.Left + 30) and
(Left > FDTArea.Left - 30) then
begin
Left := FDTArea.Left;
Right := (SelfRect.Right- SelfRect.Left) + FDTArea.Left;
end;
if (Top < FDTArea.Top + 30) and
(Top > FDTArea.Top - 30) then
begin
Top := FDTArea.Top;
Bottom := (SelfRect.Bottom - SelfRect.Top) + FDTArea.Top;
end;
if ((FDTArea.Right - Right) < 30) and
((FDTArea.Right - Right) > - 30) then
begin
Left := FDTArea.Right - (SelfRect.Right - SelfRect.Left);
Right := FDTArea.Right;
end;
if ((FDTArea.Bottom - Bottom) < 30) and
((FDTArea.Bottom - Bottom) > - 30) then
begin
Top := FDTArea.Bottom - (SelfRect.Bottom - SelfRect.Top);
Bottom := FDTArea.Bottom;
end;
// 윈도 내의 마우스 좌표의 이동값과 처음에 저장해 둔
// 값의 차이 비교..
if ((Mouse.CursorPos.X - Left) - (FStartPos.X) > 30*2) or
((Mouse.CursorPos.X - Left) - (FStartPos.X) < - 30*2) then
begin
Left := Mouse.CursorPos.X - FStartPos.X;
Right := Left + (SelfRect.Right - SelfRect.Left);
end;
if ((Mouse.CursorPos.Y - Top) - (FStartPos.Y) > 30*2) or
((Mouse.CursorPos.Y - Top) - (FStartPos.Y) < - 30*2) then
begin
Top := Mouse.CursorPos.Y - FStartPos.Y;
Bottom := Top + (SelfRect.Bottom - SelfRect.Top);
end;
end;
end;
end;
이제 위의 객체를 전역 마우스 훅에서 마우스의 버튼이 눌리고 떨어지는
경우에 따라 생성했다 해제하는 동작을 살펴보자.
*서브클래싱을 설정/해제하는 전역 마우스 훅.
// 마우스 훅 프로시저.. (전역 훅)
function MouseHookProc(Code: Integer; wParam: WPARAM;
lParam: LPARAM) : LongInt; stdcall;
begin
if Code = HC_ACTION then
begin
Case wParam of
WM_NCLBUTTONDOWN : // 윈도에서 마우스가 눌릴 때...
// 캡션에서 마우스 눌린 경우..
if MOUSEHOOKSTRUCT(Pointer(lParam)^).wHitTestCode =
HTCAPTION then
// 차일드 속성이 없고... 팝업일 때만 처리..
if (GetWindowLong(MOUSEHOOKSTRUCT(Pointer(lParam)^).hwnd,
GWL_STYLE) and WS_CHILD = 0) then
begin
if not Assigned(SubClassObj) then
SubClassObj :=
TSubClassObj.Create(
MOUSEHOOKSTRUCT(Pointer(lParam)^).hwnd);
end;
// 마우스가 떨어지는 동작은 모두 해제로 본다.
WM_NCLBUTTONUP, WM_NCRBUTTONUP, WM_NCMBUTTONUP,
WM_LBUTTONUP , WM_RBUTTONUP , WM_MBUTTONUP :
if Assigned(SubClassObj) then
begin
SubClassObj.Free;
SubClassObj := nil;
end;
end;
end;
ReadData; {파일에 저장된 마우스 훅 아이디 읽어오기.}
Result := CallNextHookEx(HMouseHook, Code, wParam, lParam);
end;
마우스 훅에서는 wParam에 메시지값이, 또 lParam에
MOUSEHOOKSTRUCT라는 마우스 정보를 담은 구조체의 포인터 값이
넘어오고 이 구조체에는 마우스 메시지가 발생한 윈도의 핸들값이
들어있으므로 SubClassObj의 생성자의 인수로 이 값을 그대로
쓰고 있다. 주의해서 봐야 할 곳으로는 서브클래싱을 걸어주는
부분에서는 조건을 조금 까다롭게, 해제는 마우스가 떨어지는
순간마다 시도하는 것과 윈도의 속성을 얻어내기 위해 사용한
GetWindowLong() API가 있겠다. 이 API는 여기 외에도
서브클래싱을 걸기 위해 이전 윈도 프로시저를 구할때도
사용했었다.
이제 이렇게 만들어진 훅 프로시저를 설치해 주면 모든 윈도우는
자석효과를 갖게 된다. 어떤가? 근사하지 않은가?
**맺으며.
32비트 서브클래싱이라는 글쓴넘도 버벅이는 주제를 건드려 보았다.
여담이지만 ‘비주얼 C++ 윈도우 셸 프로그래밍’이라는 책에서는
이 기법을 일컬어 '야만적인 방법'이라고 표현하고 있다. 이 말을
듣고 글쓴 넘은 당연히 기분이 무지무지 좋아졌었는데... 읽는
분들은 어떤 느낌일런지 궁금하다.
프로그래밍은 결국, 사용자의 요구에 대한 해결책을 제시하는
과정이고 지금 하는 조금 귀찮은 고민들이 나중에 큰 자산이 되리라
믿는다. 모쪼록 이 연재가 델파이의 멋진 라이브러리 VCL 이면에서
벌어지는 윈도우 시스템의 물밑작업을 이해하는데 조금이나마
도움이 되었으면 하고 바래본다.