윈도우즈 환경에서 특정 방향으로 윈도우 크기 조절을 제한하는 방법
2024년 9월 4일
읽는데 16분
오늘은 조금 더 특별한 케이스의 해결해야할 과제가 생겼습니다. Windows OS 타겟의 Electron 기반의 데스크톱 애플리케이션을 개발하는 상황에서, 윈도우 크기 조절을 특정한 방향으로만 수행 할 수 있도록 제한해야 하는 요구사항이 있었습니다.
이 글에서는 이러한 요구사항을 만족시키기 위해 Windows 환경에서 서브클래싱을 활용하여 윈도우 크기 조절을 제한하는 방법을 알아보겠습니다.
문제 상황
이 니즈가 발생한 가장 큰 이유는 역시 사용자 경험(UX) 측면에서의 이슈였습니다. 우리가 특정한 어플리케이션(특히 메신저 등)을 사용할 때, Window의 기본 알림 센터가 아닌 어플리케이션에서 자체 제공하는 알림 팝업을 제공하는 경우가 있습니다.
Windows 환경에서 화면에 존재하는 거의 대부분의 요소는 윈도우이므로, 이러한 팝업들도 모두 윈도우라고 생각해도 무방합니다. 이러한 팝업 윈도우의 경우, 보통은 사용자가 마음대로 크기를 조절할 수 없도록 제한하는 것이 일반적입니다.
다만, 제가 개발하는 애플리케이션의 경우, 사용자가 해당 알림 팝업 윈도우의 크기를 조절해야하는 니즈가 있었습니다. 그것 까지는 쉽게 해결 될 듯 하지만, 이 크기 조절을 특정 방향으로만 제한해야 하는 요구사항이 있었습니다.
그 이유는 사용자가 왼쪽, 위쪽이 아닌, 오른쪽 및 아래쪽으로 윈도우 크기 조절을 수행할 시 윈도우가 화면의 우측 하단에 더 이상 위치하지 않게 되어 기존 기획과는 다른 UX를 제공하게 되기 때문입니다.
해결 방법
Electron의 기능을 그대로 사용하는 방법
Electron은 말 그대로 웹 환경이므로, 이를 이용하여 BrowserWindow의 좌측, 상단에 DOM 객체를 렌더하고, 이를 리사이징 핸들로 취급하여 이벤트를 할당, main 프로세스와 통신하며 윈도우의 크기를 조절하는 방법이 있습니다.
이를 간단하게 코드로 작성해보면 다음과 같은 모습이 나올 것 입니다.
이 코드는 실제로 동작하지 않고, 이해를 위해 작성된 코드입니다.
이 코드는 간단하게 BrowserWindow의 좌측에 위치한 DOM 객체를 리사이징 핸들로 취급하여, 마우스 이벤트를 통해 윈도우의 크기를 조절하는 방법을 보여줍니다. 이 방법은 가만 보면 잘 동작할 것 같지만 실제로는 여러가지 문제점이 존재합니다.
일단 첫째로, 높은 DPI 환경에서의 문제가 있습니다. 높은 DPI 환경에서는 마우스 이벤트가 정확하게 캡쳐되지 않을 수 있으며, 이로 인해 사용자가 원하는 방향으로 윈도우 크기를 조절할 수 없는 문제가 발생할 수 있습니다.
둘째로, 성능 문제가 있습니다. 이 방법은 브라우저의 렌더링 엔진을 통해 이벤트를 캡쳐하고, 이를 main 프로세스로 전달하여 윈도우의 크기를 조절하는 OS 레벨의 로직까지 수행해야 하므로, 렌더링 성능에 영향을 미칠 수 있습니다.
이 두가지 문제를 해결하기 위해서는 완전히 다른 방법으로의 접근이 필요합니다. 바로 OS에서 제공하는 방법을 사용하는 것 입니다.
윈도우 메시지를 활용하는 방법
우리가 Win32 API 프로그래밍을 할 때, 각각의 개별 윈도우는 모두 윈도우 프로시저를 통해 메시지를 처리하는 것을 알고 있습니다.
그 중, WM_NCHITTEST
메시지는 윈도우의 클라이언트 영역에서 발생한 마우스 이벤트가 윈도우의 어느 부분에 발생했는지를 확인하는 메시지를 의미합니다.
이 메시지가 특이한 점은, 이 메시지를 처리하는 윈도우 프로시저에서 반환하는 값에 따라 예상되는 동작을 완전히 제어할 수 있다는 점입니다. 이를 이용하여 특정 방향으로의 윈도우 크기 조절을 제한할 수 있습니다.
예를 들면, 마우스가 실제로는 윈도우의 중앙에 위치하고 있지만, 윈도우 프로시저에서 HTBOTTOM
을 반환하도록 처리하면, 마치 마우스가 윈도우의 하단에 위치한 것처럼
동작하게 할 수 있습니다. 실제로 클릭시 마우스가 중앙에 위치함에도 윈도우의 크기 또한 조절 가능하게 됩니다.
이걸 역으로 생각하면, 윈도우 프로시저의 매개변수로 넘어온 값을 조사하여 실제로는 마우스가 윈도우의 하단에 위치해 있지만 HTBOTTOM
을 반환하지 않도록 처리하면,
특정 방향으로의 윈도우 크기 조절을 제한할 수 있게 됩니다.
이때 문제는 Electron의 BrowserWindow 클래스는 이러한 윈도우 프로시저를 직접적으로 제어할 수 있는 방법을 제공하지 않는다는 것입니다. 우리는 이제 어떻게 해결해야 할지 고민해야 합니다.
윈도우 메시지를 가로채는 방법들
우리가 Electron의 BrowserWindow 객체에 대해 직접적으로 윈도우 프로시저를 제어할 수 없다면, 윈도우 메시지를 가로채는 방법을 사용해야 합니다. 윈도우 메시지를 가로채는 방법은 일반적으로 세가지가 있습니다.
윈도우 메시지 후킹을 이용하는 방법
윈도우 메시지 후킹은 SetWindowsHookEx
함수를 이용하여 특정 윈도우의 메시지를 가로채는 방법입니다. 이 방법은 일반적으로는 사용되지 않는 방법입니다.
윈도우 프로시저를 교체하는 방법
윈도우 프로시저를 교체하는 방법은 SetWindowLongPtr
함수를 이용하여 특정 윈도우의 프로시저를 교체하는 방법입니다.
MSDN에서는 이 방법을 사용하는 것을 권장하지 않고 있으며, 실제로 Electron의 BrowserWindow의 윈도우 프로시저를 교체하게 되었을 때, 실제 작동 로직이 모두 변경되기 때문에 일반 동작이 전혀 수행되지 않을 수 있습니다.
서브클래싱을 이용하는 방법
서브클래싱은 SetWindowSubclass
함수를 이용하여 특정 윈도우의 프로시저를 서브클래스화하여 메시지를 가로채는 방법입니다.
이 방법은 윈도우 프로시저를 교체하는 방법과 달리, 기존의 윈도우 프로시저를 그대로 사용하면서, 특정 메시지에 대해서 추가적인 로직을 정의할 때 사용 할 수 있습니다.
그럼 이제 어떻게 서브클래싱 할까요?
기본적으로 Electron은 Node.js 환경에서 실행됩니다. 따라서 일반적인 방법으로는 Win32 API를 호출하는 것은 불가능에 가깝습니다.
하지만, Node.js는 Rust 및 C++로 네이티브 에드온을 작성 할 수 있도록 지원하고 있습니다. 이를 이용하여 Win32 API를 호출하는 네이티브 에드온을 작성하고, 이를 Electron의 main 프로세스에서 호출하는 방법을 사용할 수 있습니다.
다행히 Rust에는 매우 쉽게 Node.js 네이티브 에드온을 작성할 수 있도록 도와주는 napi-rs 라이브러리가 존재합니다. 이 라이브러리를 이용하여 Win32 API를 호출하는 네이티브 에드온을 작성 할 수 있습니다:
Rust에서 Win32 API를 호출 할 수 있도록 하는 windows crate가 존재합니다. 이를 활용하여
SetWindowSubclass
함수를 호출하여 윈도우 프로시저를 서브클래스화하고, DefSubclassProc
함수를 호출하여 기존의 윈도우 프로시저를 먼저 처리하여
결과 값을 받아옵니다.
이렇게 하면, 현재 마우스가 윈도우의 어느 부분에 위치해 있는지 확인 할 수 있으며, 이 값을 토대로 새로운 위치로서 HTCLIENT
를 반환하여 특정 방향으로의
윈도우 크기 조절을 제한할 수 있습니다.
이제 이러한 네이티브 에드온을 Electron의 main 프로세스에서 호출하여 윈도우의 프로시저를 서브클래스화하고, 특정 방향으로의 윈도우 크기 조절을 제한할 수 있게 됩니다.
이렇게 하면, 우리가 그토록 원하던 특정 방향으로의 윈도우 크기 조절을 제한하는 요구사항을 만족시킬 수 있게 됩니다.
마치며
이번 글이 다른 글에 비해 조금 더 domain-specific한 내용을 다루었고, 그로 인해 글이 조금 더 어렵고 길게 작성 되었습니다.
다만 다르게 생각해본다면, Win32 API 프로그래밍에 대한 도메인이 없었을 때, 이러한 요구사항을 만족시키기 위해 생각할 수 있는 옵션의 수가 매우 제한적이었을 것입니다.
제가 어렸을 적 Win32 API 개발을 진행했던 경험을 가진 것에 대해 감사함을 느끼며, 이번 글이 도움이 되었기를 바랍니다.
해당 글에서 사용한 코드가 실제로 구현된 라이브러리는 windows-resize-rs에서 확인하실 수 있습니다.