💻 Windows 창 크기 및 위치 조정 프로그램 만들기 (C# + Windows API)
Windows 환경에서 실행 중인 특정 프로그램의 창을 원하는 위치로 이동하거나 크기를 변경하고, 다중 모니터 간에 쉽게 이동할 수 있도록 돕는 프로그램을 제작하는 방법을 소개합니다. 또한 html 더미 플러그도 사용중인데 이경우 화면간에 빈공간안에 어떤 내용을 이동시켜둘수있는데 그때 이것을 이동하고 다시 보이는 화면으로 옮기는 용도로도 사용을 한다.
이 글에서는 Windows API를 활용한 개발 과정과 소스코드를 함께 설명하므로, 원하는 기능을 구현하는 데 많은 도움이 될 것입니다.
🏗️ 프로그램 개요
이 프로그램은 Windows API를 이용해 실행 중인 창의 핸들을 가져오고, 위치 및 크기를 조정할 수 있도록 제작되었습니다. 특히 다중 모니터 환경에서 효율적으로 창을 배치하는 기능을 제공합니다.
🖥️ 주요 기능
✅ 특정 창을 선택하여 크기 및 위치 조정
✅ 다중 모니터 간 창 이동
✅ 실행 중인 프로세스 목록 조회
✅ 마우스를 이용한 창 선택 기능 제공
🛠️ 개발 환경 및 사용 기술
📌 개발 환경: Visual Studio
📌 프로그래밍 언어: C#
📌 사용 API: Windows API, user32.dll
📁 프로젝트 구성 및 코드 설명
1️⃣ WindowsAPI.cs – Windows API 래퍼 클래스
Windows API를 직접 호출하여 특정 창의 핸들을 얻고, 크기 및 위치 정보를 가져올 수 있도록 도와주는 클래스입니다.
📌 주요 기능
- WindowFromPoint : 특정 좌표를 기준으로 창 핸들 가져오기
- GetWindowRect : 선택된 창의 크기와 위치 정보 가져오기
- GetWindowText : 창 제목 가져오기
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
namespace TargetWindowMover
{
public static class 윈도우API
{
public struct 사각형
{
public int 왼;
public int 위;
public int 오른;
public int 아래;
public 사각형(int left, int top, int right, int bottom)
{
왼 = left;
위 = top;
오른 = right;
아래 = bottom;
}
}
[DllImport("user32.dll", EntryPoint = "WindowFromPoint", SetLastError = true)]
private static extern IntPtr ImportedWindowFromPoint(Point pt);
public static IntPtr WindowFromPoint(Point point)
{
return ImportedWindowFromPoint(point);
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public static bool GetWindowRect(IntPtr hWnd, out 사각형 rect)
{
rect = new 사각형();
if (GetWindowRect(hWnd, out RECT r))
{
rect = new 사각형(r.Left, r.Top, r.Right, r.Bottom);
return true;
}
return false;
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
public static string GetWindowTextName(IntPtr hWnd)
{
StringBuilder sb = new StringBuilder(256);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
}
}
2️⃣ mousehook.cs – 마우스 후크를 활용한 창 선택 기능
마우스 클릭을 감지하여 사용자가 선택한 창의 핸들을 얻는 기능을 제공합니다.
📌 주요 기능
- 마우스 클릭 좌표에서 창 핸들 추출
- 실시간 창 선택 기능 구현
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TargetWindowMover
{
public sealed class 마우스후크 : IDisposable
{
private delegate IntPtr 저수준마우스콜백(int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr _후크 = IntPtr.Zero;
private 저수준마우스콜백 _proc;
public event MouseEventHandler? 마우스왼클릭;
private const int WH_MOUSE_LL = 14;
private const int WM_LBUTTONDOWN = 0x0201;
public 마우스후크()
{
_proc = HookFunc;
_후크 = SetHook(_proc);
}
private IntPtr HookFunc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_LBUTTONDOWN)
{
var st = Marshal.PtrToStructure<MSLLHOOK>(lParam);
마우스왼클릭?.Invoke(this, new MouseEventArgs(MouseButtons.Left, 1, st.pt.x, st.pt.y, 0));
}
return CallNextHookEx(_후크, nCode, wParam, lParam);
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x; public int y; }
[StructLayout(LayoutKind.Sequential)]
private struct MSLLHOOK
{
public POINT pt;
public uint mouseData;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private IntPtr SetHook(저수준마우스콜백 func)
{
using var ps = Process.GetCurrentProcess();
using var mod = ps.MainModule!;
return SetWindowsHookEx(WH_MOUSE_LL, func, GetModuleHandle(mod.ModuleName), 0);
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, 저수준마우스콜백 lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
public void Dispose()
{
if (_후크 != IntPtr.Zero)
{
UnhookWindowsHookEx(_후크);
_후크 = IntPtr.Zero;
}
GC.SuppressFinalize(this);
}
}
}
3️⃣ Form1.cs – 프로그램의 UI 및 기능 구현
사용자가 실행 중인 프로세스를 확인하고 원하는 창을 선택하여 크기 및 위치를 조정할 수 있도록 하는 UI와 로직을 포함합니다.
📌 주요 기능
- 실행 중인 프로세스 목록 표시
- 창 이동 및 크기 조절 기능 제공
- 마우스를 통한 창 선택 기능 추가
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TargetWindowMover
{
public partial class Form1 : Form
{
#region 창/모니터 관련 P/Invoke 및 구조체
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct MONITORINFOEX
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public char[] szDevice;
}
[StructLayout(LayoutKind.Sequential)]
public struct WINDOWPLACEMENT
{
public int length;
public int flags;
public int showCmd;
public Point ptMinPosition;
public Point ptMaxPosition;
public RECT rcNormalPosition;
}
public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool MoveWindow(IntPtr hwnd, int x, int y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData);
[DllImport("user32.dll")]
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
#endregion
// 실행 중인 프로세스를 저장할 리스트
private List<Process> runningProcesses;
// 프로세스명별 최초 감지 시의 창 크기를 저장 (이미 변경된 상태가 기본값이 될 수 있음)
private Dictionary<string, RECT> defaultRectsByName;
#region 대상창 관련 변수
private IntPtr _대상창 = IntPtr.Zero;
private IntPtr _대상자식 = IntPtr.Zero;
private 윈도우API.사각형 _창Rect;
public static 윈도우API.사각형 고정사각형;
public static Rectangle LastTargetRect = Rectangle.Empty;
#endregion
public Form1()
{
InitializeComponent();
runningProcesses = new List<Process>();
defaultRectsByName = new Dictionary<string, RECT>(StringComparer.OrdinalIgnoreCase);
LoadMonitors();
LoadRunningProcesses();
}
#region 창 이동/크기 변경 기능
private void LoadMonitors()
{
comboBoxMonitor.Items.Clear();
int monitorCount = 0;
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData)
{
MONITORINFOEX info = new MONITORINFOEX();
info.cbSize = Marshal.SizeOf(info);
GetMonitorInfo(hMonitor, ref info);
monitorCount++;
comboBoxMonitor.Items.Add("Monitor " + monitorCount);
return true;
}, IntPtr.Zero);
if (comboBoxMonitor.Items.Count > 0)
comboBoxMonitor.SelectedIndex = 0;
}
private void LoadRunningProcesses()
{
comboBoxProcess.Items.Clear();
runningProcesses.Clear();
Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
if (process.MainWindowHandle == IntPtr.Zero || !IsWindowVisible(process.MainWindowHandle))
continue;
runningProcesses.Add(process);
if (!defaultRectsByName.ContainsKey(process.ProcessName))
{
RECT normalRect;
if (GetNormalWindowRect(process.MainWindowHandle, out normalRect))
{
defaultRectsByName[process.ProcessName] = normalRect;
}
}
string windowTitle = !string.IsNullOrEmpty(process.MainWindowTitle) ? " (" + process.MainWindowTitle + ")" : "";
string displayName = process.ProcessName + windowTitle;
RECT rect;
if (GetWindowRect(process.MainWindowHandle, out rect))
{
string processInfo = $"({rect.Left}, {rect.Top}) {rect.Right - rect.Left} x {rect.Bottom - rect.Top}";
displayName += " " + processInfo;
}
comboBoxProcess.Items.Add(displayName);
}
if (comboBoxProcess.Items.Count > 0)
comboBoxProcess.SelectedIndex = 0;
}
private bool GetNormalWindowRect(IntPtr hWnd, out RECT normalRect)
{
normalRect = new RECT();
WINDOWPLACEMENT placement = new WINDOWPLACEMENT();
placement.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
if (!GetWindowPlacement(hWnd, ref placement))
return false;
normalRect = placement.rcNormalPosition;
return true;
}
private void RestoreOriginalSize(Process process)
{
IntPtr hWnd = process.MainWindowHandle;
if (hWnd == IntPtr.Zero)
{
MessageBox.Show("유효하지 않은 창입니다.");
return;
}
if (defaultRectsByName.TryGetValue(process.ProcessName, out RECT normalRect))
{
int width = normalRect.Right - normalRect.Left;
int height = normalRect.Bottom - normalRect.Top;
MoveWindow(hWnd, normalRect.Left, normalRect.Top, width, height, true);
}
else
{
MessageBox.Show("기본 창 정보를 찾을 수 없습니다.");
}
}
private void MoveWindowToMonitor(Process process, Screen screen)
{
IntPtr handle = process.MainWindowHandle;
if (handle != IntPtr.Zero)
{
RECT rect;
if (GetWindowRect(handle, out rect))
{
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
int newX = screen.WorkingArea.X + (screen.WorkingArea.Width - width) / 2;
int newY = screen.WorkingArea.Y + (screen.WorkingArea.Height - height) / 2;
MoveWindow(handle, newX, newY, width, height, true);
}
}
}
private void buttonRefresh_Click(object sender, EventArgs e)
{
LoadRunningProcesses();
}
private void buttonMove_Click(object sender, EventArgs e)
{
if (comboBoxProcess.SelectedIndex >= 0)
{
Process selectedProcess = runningProcesses[comboBoxProcess.SelectedIndex];
Screen selectedMonitor = Screen.AllScreens[comboBoxMonitor.SelectedIndex];
MoveWindowToMonitor(selectedProcess, selectedMonitor);
}
}
private void buttonSizeChange_Click(object sender, EventArgs e)
{
if (comboBoxProcess.SelectedIndex < 0)
return;
int left, top, width, height;
if (!int.TryParse(textBoxLeft.Text, out left) ||
!int.TryParse(textBoxTop.Text, out top) ||
!int.TryParse(textBoxWidth.Text, out width) ||
!int.TryParse(textBoxHeight.Text, out height))
{
MessageBox.Show("올바른 값을 입력해 주세요.");
return;
}
Process selectedProcess = runningProcesses[comboBoxProcess.SelectedIndex];
MoveWindow(selectedProcess.MainWindowHandle, left, top, width, height, true);
}
private void buttonRestore_Click(object sender, EventArgs e)
{
if (comboBoxProcess.SelectedIndex < 0)
return;
Process selectedProcess = runningProcesses[comboBoxProcess.SelectedIndex];
RestoreOriginalSize(selectedProcess);
}
private void comboBoxProcess_SelectedIndexChanged(object sender, EventArgs e)
{
if (comboBoxProcess.SelectedIndex < 0)
return;
Process selectedProcess = runningProcesses[comboBoxProcess.SelectedIndex];
RECT rect;
if (GetWindowRect(selectedProcess.MainWindowHandle, out rect))
{
textBoxLeft.Text = rect.Left.ToString();
textBoxTop.Text = rect.Top.ToString();
textBoxWidth.Text = (rect.Right - rect.Left).ToString();
textBoxHeight.Text = (rect.Bottom - rect.Top).ToString();
}
}
#endregion
#region 대상창 선택 (마우스 후크 이용)
private void buttonTargetSelect_Click(object sender, EventArgs e)
{
this.Hide();
bool done = false;
using (var hook = new 마우스후크())
{
hook.마우스왼클릭 += (s2, e2) =>
{
var hWnd = 윈도우API.WindowFromPoint(new Point(e2.X, e2.Y));
if (hWnd != IntPtr.Zero)
{
_대상창 = hWnd;
_대상자식 = hWnd;
if (윈도우API.GetWindowRect(_대상창, out _창Rect))
{
고정사각형 = _창Rect;
LastTargetRect = new Rectangle(_창Rect.왼, _창Rect.위, _창Rect.오른 - _창Rect.왼, _창Rect.아래 - _창Rect.위);
Log($"[창선택] hWnd=0x{_대상창:X}, title={윈도우API.GetWindowTextName(_대상창)}");
uint procId;
GetWindowThreadProcessId(_대상창, out procId);
try
{
Process proc = Process.GetProcessById((int)procId);
for (int i = 0; i < runningProcesses.Count; i++)
{
if (runningProcesses[i].Id == proc.Id)
{
comboBoxProcess.SelectedIndex = i;
break;
}
}
}
catch (Exception ex)
{
Log("프로세스 검색 실패: " + ex.Message);
}
}
else
{
Log("GetWindowRect 실패");
}
}
else
{
Log("유효한 창 아님");
}
done = true;
};
while (!done)
{
Application.DoEvents();
System.Threading.Thread.Sleep(30);
}
}
this.Show();
}
#endregion
private void Log(string message)
{
Console.WriteLine(message);
}
}
}
🚀 프로그램 제작 단계
✔️ 1단계: Windows API 연동
- WindowsAPI.cs를 구현하여 창 핸들을 가져오는 기능을 추가합니다.
✔️ 2단계: 마우스 후크 구현
- mousehook.cs를 활용해 사용자가 원하는 창을 클릭하면 자동으로 선택되는 기능을 추가합니다.
✔️ 3단계: UI 및 기능 구현
- Form1.cs에서 창을 이동하고 크기를 조정할 수 있는 인터페이스를 만듭니다.
🔧 프로그램 사용법
1️⃣ 프로그램을 실행하면 실행 중인 프로세스 목록이 표시됩니다.
2️⃣ 이동할 창을 선택한 뒤, 원하는 모니터를 선택합니다.
3️⃣ "창 이동" 버튼을 클릭하여 선택한 창을 특정 위치로 이동합니다.
4️⃣ 크기를 변경하려면 원하는 값 입력 후 "사이즈 변경" 버튼을 누릅니다.
5️⃣ 마우스로 직접 창을 선택하고 싶다면 "창 선택" 버튼을 누른 후 원하는 창을 클릭하면 됩니다.
📌 전체 소스코드 다운로드
아래 링크에서 전체 프로젝트 파일을 다운로드할 수 있습니다.
📂 소스코드 다운로드:
🔗 Form1.cs
🔗 Form1.Designer.cs
🔗 윈도우API.cs
🔗 mousehook.cs
⚠️ 주의사항
✔️ 관리자 권한으로 실행 필요 – 일부 시스템 창은 보호되어 있어 관리자 권한이 없으면 이동 및 크기 변경이 제한될 수 있습니다.
✔️ 보안 경고 발생 가능 – 마우스 후크 기능을 사용할 경우 일부 백신에서 보안 경고가 발생할 수 있습니다.
✔️ P/Invoke 사용 시 주의 – Windows API 호출 시 잘못된 접근 방식은 프로그램 충돌을 유발할 수 있습니다.
📚 참고 자료
🔗 Windows API 공식 문서
🔗 C# P/Invoke 사용법
이 프로그램을 활용하면 Windows 환경에서 창 관리가 훨씬 편리해집니다. 궁금한 점이 있다면 댓글로 남겨주세요! 😊💬
'취미 > C#' 카테고리의 다른 글
ShareX: 윈도우 캡처 끝판왕! 생산성 200% 올리는 방법 (51) | 2025.05.07 |
---|---|
📦 BackgroundEraser 프로그램 소개 및 사용법 (4) | 2025.03.21 |