WPF 프로젝트에서 Win32 API를 이용하여 Mouse를 제어할 일이 생겼습니다.
MFC를 해본 적이 없고, Win32 API에 대한 이해도가 낮았기 때문에 조금 고생해서, 이력을 남겨 놓습니다.
ChatGPT에게 Win32 API를 이용해서 MouseMove를 하는 방법을 물어보면 3가지 방법을 추천해줍니다.
- SendMessage/PostMessage 이용하는 방법
- mouse_event 이용하는 방법
- SendInput 이용하는 방법
이 중에서 1번을 시도했다가 동작하지 않아서 3번으로 바꿨습니다. (왜 동작하지 않았는지는 모르겠습니다.)
mouse_event를 고려하지 않은 이유는,
mouse_event 함수(winuser.h) - Win32 apps | Microsoft Learn
이것을 확인했기 때문입니다. Deprecated인지는 모르겠지만요.
Mouse 제어 예제이지만 코드 자체는 Key 제어의 코드도 같이 올리겠습니다.
준비과정
Win32 API 의 SendInput에서는 INPUT이라는 공용체(union)을 사용해서 입력을 제어하고 있습니다.
C#에서는 공용체를 정의하지 못하므로, ChatGPT에게 물어서 아래와 같이 작성했습니다.
INPUT32
[StructLayout(LayoutKind.Explicit)]
public struct INPUT32
{
[FieldOffset(0)]
public uint type; // x86에서 int, uint는 4byte이다.
[FieldOffset(4)]
public KEYBDINPUT ki;
[FieldOffset(4)]
public MOUSEINPUT mi;
[FieldOffset(4)]
public HARDWAREINPUT hi;
}
INPUT64
[StructLayout(LayoutKind.Explicit)]
public struct INPUT64
{
[FieldOffset(0)]
public uint type; // x64에서 int, uint는 8byte이다.
[FieldOffset(8)]
public KEYBDINPUT ki;
[FieldOffset(8)]
public MOUSEINPUT mi;
[FieldOffset(8)]
public HARDWAREINPUT hi;
}
KEYBDINPUT
[StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
MONITORINFO
[StructLayout(LayoutKind.Sequential)]
public struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
MOUSEINPUT
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
아키텍쳐가 다를 것을 고려해서 INPUT 객체를 32bit용 64bit용으로 2개 만들었습니다.
사실 INPUT 클래스를 위처럼 공용체 스타일로 쓰지 않고, 마우스, 키보드에 대해 따로 만들면 INPUT 클래스 자체를 [StructLayout(LayoutKind.Sequential)] 처리할 수 있기 때문에 Sequential 뜻대로 직렬화되어 x86, x64 아키텍쳐에서 모두 동작합니다.
하지만 욕심을 내서공용체로 작성하려면 [StructLayout(LayoutKind.Explicit)]을 사용해야 하기 때문에 이 타입은 몇 바이트라는 것을 명시해주어야 하고 그것이 [FieldOffset(4)] 입니다.
주석을 보시면 아시겠지만 x86과 x64에서 int의 기본 사이즈는 다릅니다. 제 현재 PC가 x64이고, Explict으로 선언된 공용체를 써서 [FieldOffset(4)]로 선언된 클래스를 사용하면 아래의 SendInput 메서드는 동작하지 않습니다.
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT32[] pInputs, int cbSize);
그래서 하나 더 선언합니다.
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT32[] pInputs, int cbSize);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT64[] pInputs, int cbSize);
여기까지가 Win32 API로 SendInput을 사용하기 위한 준비과정입니다.
SendInput의 정규화
INPUT32와 INPUT64에 들어가는 MOUSEINPUT 값은 정규화를 해야합니다. 아래와 같은 규칙이 있기 때문입니다.
MOUSEINPUT(winuser.h) - Win32 apps | Microsoft Learn

위 문서를 보면 MOUSEEVENTF_ABSOLUTE 를 사용하지 않아도 되는 것처럼 설명이 써있지만, 이 옵션을 사용하지 않으면 절대좌표가 아니라 상대좌표 형태로 동작하기 때문에 오히려 제어가 어렵습니다. 따라서 반드시 써야하는 옵션입니다. 사용하게 되면 아래와 같이 작성이 됩니다.
private static INPUT32 MouseMove32(int normalized_X, int normalized_Y)
{
// 마우스 이동 입력 생성
var mouseMoveInput = new INPUT32
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dx = normalized_X,
dy = normalized_Y,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE
}
};
return mouseMoveInput;
}
private static INPUT64 MouseMove64(int normalized_X, int normalized_Y)
{
// 마우스 이동 입력 생성
var mouseMoveInput = new INPUT64
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dx = normalized_X,
dy = normalized_Y,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE
}
};
return mouseMoveInput;
}
private static INPUT32 MouseDown32()
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 누름)
var mouseDownInput = new INPUT32
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN }
};
return mouseDownInput;
}
private static INPUT64 MouseDown64()
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 누름)
var mouseDownInput = new INPUT64
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN }
};
return mouseDownInput;
}
private static INPUT32 MouseDown32(int x, int y)
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 누름)
var mouseDownInput = new INPUT32
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dx = x, dy = y, dwFlags = MOUSEEVENTF_LEFTDOWN }
};
return mouseDownInput;
}
private static INPUT64 MouseDown64(int x, int y)
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 누름)
var mouseDownInput = new INPUT64
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dx = x, dy = y, dwFlags = MOUSEEVENTF_LEFTDOWN }
};
return mouseDownInput;
}
private static INPUT32 MouseUp32()
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 뗌)
var mouseUpInput = new INPUT32
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP }
};
return mouseUpInput;
}
private static INPUT64 MouseUp64()
{
// 마우스 클릭 입력 생성 (왼쪽 버튼 뗌)
var mouseUpInput = new INPUT64
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP }
};
return mouseUpInput;
}
private static INPUT32 KeyDown32(short vk)
{
var keyDownInput = new INPUT32
{
type = INPUT_KEYBOARD,
ki = new KEYBDINPUT { wVk = (ushort)vk }
};
return keyDownInput;
}
private static INPUT64 KeyDown64(short vk)
{
var keyDownInput = new INPUT64
{
type = INPUT_KEYBOARD,
ki = new KEYBDINPUT { wVk = (ushort)vk }
};
return keyDownInput;
}
private static INPUT32 KeyUp32(short vk)
{
var keyUpInput = new INPUT32
{
type = INPUT_KEYBOARD,
ki = new KEYBDINPUT { wVk = (ushort)vk, dwFlags = KEYEVENTF_KEYUP }
};
return keyUpInput;
}
private static INPUT64 KeyUp64(short vk)
{
var keyUpInput = new INPUT64
{
type = INPUT_KEYBOARD,
ki = new KEYBDINPUT { wVk = (ushort)vk, dwFlags = KEYEVENTF_KEYUP }
};
return keyUpInput;
}
애초에 WindowHandle 기반인 SendMessage를 사용하려고 했기 때문에 Handle을 이미 얻은 상태입니다. 그러면 아래와 같이 MouseClick을 위한 코드를 만들 수 있습니다.
public static void ProcessInMouseClick(IntPtr targetProcessHandle, int x, int y)
{
IntPtr hMonitor = MonitorFromWindow(targetProcessHandle, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = Marshal.SizeOf(monitorInfo);
if (!GetMonitorInfo(hMonitor, ref monitorInfo)) return;
if (!GetWindowRect(new HandleRef(null, targetProcessHandle), out RECT rect)) return;
int offset_window_posX = rect.Left;
int offset_window_posY = rect.Top;
int _x = x + offset_window_posX;
double ratioX = (double)_x / monitorInfo.rcMonitor.Width;
int normalizedX = Convert.ToInt32(Math.Round(ratioX * NORMALIZED_VALUE));
int _y = y + offset_window_posY;
double ratioY = (double)_y / monitorInfo.rcMonitor.Height;
int normalizedY = Convert.ToInt32(Math.Round(ratioY * NORMALIZED_VALUE));
if (Environment.Is64BitProcess)
SendInput(1, new[] { MouseMove64(normalizedX, normalizedY) }, Marshal.SizeOf(typeof(INPUT64)));
else
SendInput(1, new[] { MouseMove32(normalizedX, normalizedY) }, Marshal.SizeOf(typeof(INPUT32)));
if (Environment.Is64BitProcess)
SendInput(1, new[] { MouseDown64() }, Marshal.SizeOf(typeof(INPUT64)));
else
SendInput(1, new[] { MouseDown32() }, Marshal.SizeOf(typeof(INPUT32)));
if (Environment.Is64BitProcess)
SendInput(1, new[] { MouseUp64() }, Marshal.SizeOf(typeof(INPUT64)));
else
SendInput(1, new[] { MouseUp32() }, Marshal.SizeOf(typeof(INPUT32)));
}
※RECT는 따로 정의한 것입니다.
이 작업을 할 때는 Multi Monitor 상황을 고려해야 합니다.
모니터가 1개라면 신경쓰지 않아도 되지만, 듀얼 모니터 이상이라면 Windows의 전체 공간의 좌표값이 필요하기 때문이고 그 좌표계는 이어 붙혀져 있기 때문입니다.
- 클릭하고 싶은 대상의 UI 핸들을 얻음
- 핸들이 존재하는 모니터의 핸들을 얻음 (프로세스 핸들 아님)
- 모니터의 크기를 가져옴
- UI 핸들이 존재하는 모니터 내부에서의 Left와 Top 좌표값을 획득
- 좌표값을 계산
중요한 것은 5번의 좌표값을 계산할 때입니다.
좌표값은 모니터의 크기에 따라 비율이 달라집니다. (당연히 좌표 자체는 달라지지 않습니다.)
무슨 말이냐면, 내가 (100, 100) 이라는 좌표를 클릭하고 싶었는데 이것은 내 현재 모니터 해상도인 (1920 * 1080) 일 때 100,100 의 좌표를 원하는 것이지 사람이 원하는 것은 좌표가 아니라 모니터 안에서 어디에 위치해 있는지 입니다. 따라서 해상도가 커지면 사람이 원하는 위치는 (100, 100) 보다 커지게 됩니다.
즉, ratio를 봐야한다는 말입니다.
비율을 구하기 위해서는 고정된 좌표값인 int값을 반드시 전체 크기로 나눠야합니다.
정수를 나누는 과정에서는 소숫점이 발생할 수 있는데 이점이 중요합니다.
아까 정규화를 하기 위해 0~65535의 좌표계를 사용해야 모니터의 비율에 맞출 수 있다고 했는데요.
현재 내 UI 핸들의 좌표값을 정했어도 이것을 전체 모니터 크기로 나눠서 비율을 만들고, 그 값에 65536을 곱해서 절대 좌표로 변환한 값을 찍어야 SendInput을 통해 MouseMove가 가능합니다.
절대 좌표는 65535까지로 정해져있기 때문에 해상도가 커져도 이 값이 늘어나지 않습니다. 늘어나는 것은 절대좌표로 나눈 65535 Point 들의 1 Point 당 면적이 넓어집니다.
따라서 소숫점을 계산하기 전부터 최대한 잘 가지고 있다가 마지막에 정규화할때 그 때 비로소 반올림을하거나 버릴 것을 결정해야 합니다.
이전부터 소숫점 다 잘라먹으면서 편하게 계산하면 마우스의 정확한 제어가 불가능합니다.
위 수식의 계산 순서를 잘 보면 도움이 되실 것 같습니다.
사실 이렇게 했어도 완벽하게 정밀하지는 않습니다.
더욱 정밀한 마우스 포인터 제어가 있다면 소개 부탁드립니다.
