C 코드를 C# 코드로 고쳐 작성하는 것이 정말 쉽지는 않습니다. C/C++ 코드에서 LRESULT CALLBACK 키워드로 처리한 부분을 C# 코드로 표현하는 것과, SetWindowsHookEx 함수의 두 번째 인자를 설정하는 것은 약간의 기술이 필요할 수도 있습니다. 한 함수의 주소를 다른 함수의 인자로 사용하는 것이 C# 언어에서 매우 어려울 수 있습니다.
이것을 해결하기 위하여, 두 가지 관점에서 접근해 보겠습니다. 첫 번째 방법은 컴파일러의 오류 메시지를 적극 활용해 보는 것입니다.
SetWindowsHookEx 함수와 HOOKPROC 형식의 Win32 선언은 다음과 같습니다.
HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);
typedef LRESULT (CALLBACK* HOOKPROC)(int code, WPARAM wParam, LPARAM lParam);
C# 코드에서 SetWindowsHookEx 함수를 다음과 같이 선언하고, 호출 코드를 작성합니다.
[DllImport(“user32.dll”, EntryPoint = “SetWindowsHookEx”, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr SetWindowsHookEx(Int32 idHook, IntPtr lpfn, IntPtr hMod, Int32 dwThreadId);
…… hhookSysMsg=SetWindowsHookEx(WH_CBT,(IntPtr)CBTHookProc,IntPtr.Zero,GetCurrentThreadId());
|
컴파일러는 다음과 같은 오류를 내보낼 것입니다.
오류 CS0428: ‘CBTHookProc’ 메서드 그룹을 비대리자 형식’System.IntPtr'(으)로 변환할 수 없습니다. 메서드를 호출하시겠습니까?
|
인자를 대리자로 전달하고, 처리되는 함수를 대리자가 호출하는 프로시저 형태로 선언하면 될듯합니다.
두 번째 방법은 윈도우의 전형적인 Event Handler 프로시저를 고려하는 것입니다. C 코드에서 사용한 CBTProc 함수는 WH_CBT 메시지(이벤트)에 대한 처리 프로시저라는 것을 기억해야 합니다. 그럼, .NET Framework 라이브러리에서 제공되는 이벤트 처리 함수의 선언과 동일하게 하면 될 것 같습니다. 이벤트 처리기(Event Handler)는 대리자를 통해 호출되는 프로시저(Method)입니다.
Windows Forms 응용프로그램에서, 각종 컨트롤의 이벤트와 관련되어 다음과 같은 코드를 볼 수 있습니다.
this.button1.Click += new System.EventHandler(this.button1_Click);
EventHandler 키워드를 이용하면 될듯합니다. 이 키워드에 대해 조금 더 자세히 살펴보기 위하여, 정의 부분을 탐색해보겠습니다.
public delegate void EventHandler(object sender, EventArgs e);
반환 값이 설정되어 있지 않은 delegate 함수였습니다. C 코드의 LRESULT 형식을 IntPtr 형식으로 변환하여 다음과 같이 이벤트 처리기 함수를 선언합니다.
public delegate IntPtr HookProc(Int32 nCode, IntPtr wParam, IntPtr lParam);
또, SetWindowsHookEx 함수의 두 번째 인자는 다음과 같이 수정합니다.
public static extern IntPtr SetWindowsHookEx(Int32 idHook, HookProc lpfn, ……);
*주의: SetWindowsHookEx 함수의 마지막 인자에 .NET 프레임워크에서 제공하는 AppDomain.GetCurrentThreadId() 함수를 사용하여 컴파일하면, 다음과 같은 경고 메시지를 표시합니다.
경고 CS0618: ‘System.AppDomain.GetCurrentThreadId()’은(는) 사용되지않습니다. ‘AppDomain.GetCurrentThreadId has been deprecated because it does not provide a stable Id when managed threads are running on fibers (aka lightweight threads). To get a stable identifier for a managed thread, use the ManagedThreadId property on Thread. http://go.microsoft.com/fwlink/?linkid=14202’
|
이 메시지대로, ManagedThreadId 속성을 사용하면, 프로그램은 정상적인 동작을 하지 않게 됩니다. 함수가 반환하는 ID 값과 속성이 가진 ID 값이 다르기 때문입니다. 이것에 대하여 많은 사람들의 논란이 있는데, 정작 중요한 것은 경고 메시지의 지시대로 했다가는 프로그램은 동작하지 않는다는 것입니다. 그럼, 위의 경고를 회피하고, 처리하는 방법은 없을까요? 컴파일 지시어로 CS0618 경고를 무시하도록 할 수는 있습니다. 그러나, 근본적인 해결은 될 수 없습니다.
Win32 API 함수를 사용해 보겠습니다. 아래의 함수를 이용하여, 획득한 값은 정확히 ‘System.AppDomain.GetCurrentThreadId()’ 함수를 사용하여 얻은 값과 일치합니다.
[DllImport(“Kernel32.dll”, EntryPoint= “GetCurrentThreadId”, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern Int32 GetCurrentThreadId();
|
아래와 같이, 코드에서 사용할 데이터 형식과 기타 Win32 API 함수를 선언합니다.
private const Int32 HCBT_CREATEWND = 3;
private const Int32 WH_CBT = 5;
[StructLayout(LayoutKind.Sequential)]
public struct CBT_CREATEWND
{
public IntPtr lpcs; // CREATESTRUCT
public IntPtr hwndInsertAfter;
}
|
C# 언어로 구현된 WH_CBT 메시지 처리 프로시저의 구현 부분을 C 코드와 비교하여 보겠습니다.
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
LPCREATESTRUCT lpcs;
LPCBT_CREATEWND lpcbtcw;
HWND hNewhWnd;
LPCTSTR pszClassName;
CHAR szClassName[_MAX_PATH];
WNDPROC oldWndProc;
switch (nCode) {
case HCBT_CREATEWND:
lpcbtcw = (LPCBT_CREATEWND)lParam;
hNewhWnd = (HWND)wParam;
lpcs = lpcbtcw->lpcs;
if ((unsigned long)(lpcs->lpszClass) > 0xffff) {
pszClassName = lpcs->lpszClass;
} else {
*szClassName = 0x00;
//GlobalGetAtomName((ATOM)lpcs->lpszClass, szClassName, _countof(szClassName));
GetClassName(hNewhWnd, szClassName, _countof(szClassName));
pszClassName = szClassName;
}
if (!lstrcmpi(pszClassName, “#32770”) && !(lpcs->style & WS_CHILD)) {
lpcs->cx = 200;
lpcs->cy = 200;
oldWndProc = (WNDPROC)GetWindowLongPtr(hNewhWnd, GWLP_WNDPROC);
if (oldWndProc != NULL && GetProp(hNewhWnd, _afxOldWndProc) == NULL) {
SetProp(hNewhWnd, (LPCTSTR)_afxOldWndProc, (HANDLE)oldWndProc);
if ((WNDPROC)GetProp(hNewhWnd, _afxOldWndProc) == oldWndProc) {
GlobalAddAtom(_afxOldWndProc);
SetWindowLongPtr(hNewhWnd, GWLP_WNDPROC, (LONG)WndProc32770);
}
}
}
break;
}
return CallNextHookEx(hhookSysMsg, nCode, wParam, lParam);
}
|
public static IntPtr CBTHookProc(Int32 nCode, IntPtr wParam, IntPtr lParam)
{
CBT_CREATEWND cbtcw;
IntPtr hNewhWnd = IntPtr.Zero;
StringBuilder sbClassName = null;
IntPtr oldWndProc = IntPtr.Zero;
switch (nCode) {
case HCBT_CREATEWND:
cbtcw = (CBT_CREATEWND)Marshal.PtrToStructure(lParam, typeof(CBT_CREATEWND));
hNewhWnd = wParam;
sbClassName = new StringBuilder(MAX_PATH);
GetClassName(hNewhWnd, sbClassName, MAX_PATH);
if (String.Equals(sbClassName.ToString(), “#32770”)) {
oldWndProc = (IntPtr)GetWindowLong(hNewhWnd, GWLP_WNDPROC);
if((oldWndProc!=IntPtr.Zero)&&(GetProp(hNewhWnd, sOldWndProc)==IntPtr.Zero)) {
SetProp(hNewhWnd, sOldWndProc, oldWndProc);
if (GetProp(hNewhWnd, sOldWndProc) == oldWndProc) {
GlobalAddAtom(sOldWndProc);
SetWindowLongProc(hNewhWnd, GWLP_WNDPROC, WndProc32770);
}
}
}
break;
}
return CallNextHookEx(hhookSysMsg, nCode, wParam, lParam);
}
|
C 코드에서는 LPCBT_CREATEWND 포인터에서 멤버인 LPCREATESTRUCT 포인터에 접근하는 것이 가능했습니다.
C# 코드에서 이런 작업을 수행하기 위하여, CBTHookProc 함수의 lParam 인자를, Marshal.PtrToStructure 함수를 이용하여 CBT_CREATEWND 구조체로 변환합니다. C 코드처럼 접근하기 위하여, 구조체의 멤버인 lpcs 개체에 대하여 다음과 같은 코드를 작성합니다.
CREATESTRUCT cs = (CREATESTRUCT)Marshal.PtrToStructure(cbtcw.lpcs, typeof(CREATESTRUCT));
|