프날 오토핫키 강좌
누르면 강좌 리스트가 나와요
프날 오토핫키 강좌

⚠ 이 강좌는 오토핫키 v1을 다룹니다

지금 보시는 강좌는 구버전 오토핫키(v1.1)를 다루고 있습니다. 따라서 본 강좌의 내용은 현재 최신 오토핫키 버전 (v2.0)과 호환되지 않습니다. 구버전의 정보가 필요한 것이 아니라면, 가능한 한 새로운 사이트에 작성한 v2 강좌(https://ahkv2.pnal.dev)를 봐주시길 바랍니다.

[프날 오토핫키] GDI+ (gdip) #6: 특정 창 이미지 가져오기

지난 강좌에서는 스크린 전체, 혹은 특정 영역에서 비트맵을 가져오는 함수로 Gdip_BitmapFromScreen()함수를 배웠습니다.

이번 강좌는 조금 쉬어가는 시간으로, 스크린 기준으로 비트맵을 가져오는 것이 아닌 특정 창에서 비트맵을 가져오는 방법을 배워보겠습니다.

 


 윈도우 핸들(hWnd) 

우선적으로, '핸들'에 대해서 이해하셔야합니다. 이미 알고계신 분은 이 부분을 건너뛰셔도 좋습니다.

 

원래 '핸들'은 서로 다른 구성 요소끼리 겹치지 않는 고유의 이름을 뜻합니다. 종류에 따라 파일 핸들, 윈도우 핸들, 그래픽 객체 핸들(ex: 펜 핸들, 브러시 핸들) 등으로 불립니다.

 

이 중에서도 우리가 다룰 '윈도우 핸들'이란, 이름에서도 알 수 있듯 창끼리 겹치지 않는 고유 넘버입니다. 같은 타이틀을 가진 창은 있을 수 있어도, 같은 핸들값을 가진 창은 없다는 뜻입니다. 따라서, 핸들 값을 이용하면 특정한 창에서만 비트맵을 가져올 수 있습니다.

 

여담으로, 윈도우 핸들을 hWnd라고도 하는데, h(handle)와 Wnd(Window)가 합쳐진 말입니다.


 타이틀로 핸들 구하는 법 

GDI+는 윈도우 타이틀보단 핸들 값으로 윈도우를 특정 짓는 경우가 많습니다. 따라서 우리는 타이틀만으로 핸들 값을 구할 수 있어야 GDI+에서 써먹을 수 있습니다.

 

오토핫키에는 내장 된 명령어로 크게 두 가지 방법이 있는데, 하나는 WinGet 명령어를 이용하는 것이고 하나는 WinExist()함수를 이용하는 것입니다.

 

우리는 WinExist() 함수를 이용하여 구해보겠습니다. 내장되어있는 함수이기 때문에 따로 정의할 필요는 없습니다.

  • 매개 변수: 핸들 값을 구하고자 하는 창의 타이틀
  • 반환 값: 해당 타이틀을 가진 창의 핸들 값

예를 들어, 아래와 같은 식입니다.

hWnd := WinExist("제목 없음 - Windows 메모장")

→ 변수 hWnd에는 "제목 없음 - Windows 메모장"의 핸들 값이 들어갑니다.

 


 특정 창에서 비트맵 가져오기 

이제 본격적으로 특정 창에서 비트맵을 가져오겠습니다. 저번 강에서 배웠던 함수와 비슷한 이름인 Gdip_BitmapFromHWND() 함수를 이용하겠습니다.

Gdip_BitmapFromHWND()

기능: 특정 핸들 값을 가진 창에서 비트맵을 가져온다.
  • 매개 변수: 비트맵을 가져 올 창의 핸들 값
  • 반환 값: 가져온 비트맵의 메모리 주소

감이 오시죠? 저번 강에서와 방법은 동일한데, Gdip_BitmapFromScreen()이 아닌 Gdip_BitmapFromHWND()를 써 주어야 한다는 점과 핸들 값을 구하는 과정을 먼저 해주어야 한다는 점이 다르네요.

 

#Include Gdip_All.ahk
pToken := Gdip_Startup()

pBitmap := Gdip_BitmapFromHwnd(WinExist("제목  없음 - Windows 메모장"))
Gdip_SaveBitmapToFile(pBitmap, A_Desktop "\result.png")
Gdip_DisposeImage(pBitmap)

Gdip_Shutdown(pToken)
ExitApp

Test.ahk
0.00MB

지난 강에서 배웠던 Gdip_SaveBitmapToFile()을 통해 가져온 비트맵을 파일로 저장해주었고, 더이상 쓰지 않는 비트맵을 Dispose해주었습니다.

 

Gdip_BitmapFromHWND()함수의 매개변수로 WinExist()함수를 직접 넣어줌으로써 핸들 값을 넘겨주었다는 점 또한 보실 수 있습니다.


 덧붙임: "캡처한 화면이 검은색이에요" 

어떤 창을 캡처하느냐에 따라 검은색으로만 캡처되는 경우가 있습니다. 예를 들어서 여러분이 지금 보시는 인터넷 창을 캡처하시면, 크롬 계열 브라우저를 사용하는 경우엔 검은색으로만 찍히는 분이 많을 것입니다. (브라우저 설정에 따라 정상적으로 찍힐때도 있어요.)

 

이를 이해하려면 우선 Gdip_BitmapFromHwnd()가 어떻게 작동하는지 알아야합니다. 정확히는 GDI+에서 창의 비트맵을 어떻게 가져오는지를 알아야합니다. 사실 보이지 않는 창을 캡처한다는게 상식적으로 이해되지는 않죠? 다른 창에 가려져있는데 어떻게 그 뒤에 있는 창의 비트맵을 가져올 수 있는걸까요?

 

이를 설명하려면 GDI에서 사용되는 여러 개념을 이해해야하지만, 간단하게만 설명드리겠습니다.

 

여러분이 지금 열려있는 프로그램의 화면은 항상 렌더링된(=화면에 뿌려진) 창입니다. 당연한 이야기죠. 그래야지 모니터에서 프로그램의 창을 볼 수 있으니까요. 우리가 프로그램을 실행시키면 프로그램에 필요한 여러 구성요소(컨트롤, 뷰 등)를 잘 조립시킨 다음에, 이 이미지를 기억장치가 기억하고 있습니다. 화면에 구성 요소를 조립하는 과정을 '렌더링'이라고 합니다. 즉, 프로그램이 실행되면 창이 렌더링 되어 화면에 보여지고, 그 이미지는 기억장치가 가지고 있다는 것이지요.

 

렌더링 방법은 여러가지인데, 그 중 하나가 GDI입니다. 그런데 GDI로 렌더링되는 프로그램만 있는 것은 아니거든요. DirectX나 OpenGL처럼 여러 방법을 이용해서 창을 렌더링하는 프로그램도 있습니다. 어떤 방식을 사용할지는 해당 프로그램따라 다릅니다. 즉, 어떤 방식을 써도 프로그램의 구성 요소를 화면에 잘 조립해서 보여주나, 그 선택은 프로그래머가 하겠죠.

 

보통 DirectX나 OpenGL은 멀티미디어를 위해 사용됩니다. 동영상 재생이라던가 에뮬레이션, 게이밍 등의 작업에 사용되지요. 이유는 성능 때문입니다. GDI는 사실 이 둘에 비해서는 굉장히 느린 방식이고, CPU와 RAM을 사용해서 화면에 렌더링하게 됩니다. 하드웨어에 기초지식이 있으신 분들은 이해하겠지만, CPU는 화면에 프로그램 창(비트맵)을 뿌려주거나 구성하여 이미지로 만드는 '렌더링'을 하고, RAM은 CPU가 렌더링된 비트맵을 '기억하는 작업'을 합니다.

 

그런데 DirectX나 OpenGL은 그렇지 않습니다. CPU 대신 GPU(흔히 '그래픽 카드'라는 부품에 붙어있습니다.)를 사용하여 렌더링하고, RAM 대신 VRAM을 사용하여 기억합니다. 역시 성능적인 이유라고 생각하면 됩니다. 그런데 이 차이때문에, DirectX나 OpenGL 등을 사용하여 렌더링되는 프로그램은 GDI로 비트맵을 복사해올 수 없습니다. VRAM공간에 GDI가 접근할 수 없기 때문입니다. (다시 말씀드리면, GDI는 비트맵을 가져올 때 VRAM이 아닌 RAM에만 접근합니다.)

 

따라서, 결론적으로, DirectX나 OpenGL 등 다른 방식으로 렌더링되는 프로그램은 GDI를 이용해서 비트맵을 복사해올 수 없습니다. 이는 각각의 방식이 서로 다른 '비트맵 기억공간'을 사용하기 때문이고(RAM vs VRAM), 따라서 GDI로는 VRAM에 있는 '렌더링 이미지'를 가져올 수 없기 때문에 검은 화면으로만 가져와지는 것입니다. 특히 DirectX나 OpenGL은 앞서 말씀드렸듯 멀티미디어 프로그램에서 자주 쓰이지요.

 

이를 해결하는 방법은 없습니다. 근본적인 문제이기 때문입니다. 단! 프로그램에 따라, 특히 게임을 제외한 멀티미디어 프로그램(동영상 플레이어, 웹브라우저, 뷰어 등)은 DirectX나 OpenGL로 렌더링하는 것을 끌 수 있는 옵션이 존재하는 경우가 많습니다. 이 옵션을 끄면 GDI로 렌더링되게 되고, 그런 경우엔 이 강좌의 방식으로 창의 비트맵을 가져올 수 있는 것이지요. 크롬에선 이 옵션이 '하드웨어 가속'이라는 이름으로 있습니다. 이 옵션이 켜지면 DirectX로, 꺼지면 GDI로 렌더링됩니다. 다만 이런 옵션을 끌 경우 해당 프로그램의 성능 하락(멀티미디어 재생에서의 끊김 등)이 나타날 수 있습니다.

 

그래서 해결이 안되는 경우엔, 그냥 창을 화면에 보이게 한다음에 이전 강에서 말씀드린 Gdip_BitmapFromScreen()을 사용하세요. 화면에 렌더링 된 다음에 운영체제 화면의 일부를 가져오는 것은 GDI도 할 수 있습니다. 물론 여러분이 DirectX를 이용하거나 하면 DirectX로 렌더링 된 프로그램의 창 비트맵도 가져올 수 있겠으나, 이미 그정도 API를 이용할 줄 아시면 현업에서 프로그래밍을 하시고 계시거나, 오토핫키가 아닌 타 언어를 손쉽게 다룰 수 있는 사람일 것입니다. 오토핫키는 어디까지나 라이트한... 스크립트 언어이기 때문에, 이런 경우엔 타언어로 만들어보시는 것을 추천합니다.


 저장된 사진이랑 실제 UI랑은 다릅니다 

** 여담입니다. 안 읽으셔도 됩니다. **

 

위 예제로 저장된 사진을 보시면, 실제로 여러분들이 보시던 창과는 다르게 캡처되어있는 모습을 볼 수 있습니다. 물론 안 그런 사람도 있고요.

 

GDI+에는 printWindow라는 가상의 창을 제작해서 붙여주는 함수가 있습니다. (이렇게 간단히 설명드리지 못하는 건데, 조금 많이 잘못 설명드려보았습니다. DC라는 것의 개념을 아셔야지 설명이 되어서요.) Gdip_BitmapFromHWND는 이 printWindow함수를 참조하는데, 이 함수가 만들어내는 '가상의 창'이 아마 여러분의 UI와는 좀 다를 것입니다. 뭐 같을수도 있고요. 이건 PC환경의 차이니까요.

 

Windows 10이 대중화된 요즘엔 이 윈도우 테두리의 차이로 인해 저장된 이미지와 실제 윈도우끼리의 좌표 차이가 조금 날 것 처럼 보이는 것도 사실입니다. 실제로 사진만 놓고 보면 가로 8px쯤 차이가 납니다.

 

하지만 괜찮습니다. 여러분이 Windows 10을 쓰고 있더라도, 실제 여러분이 보시는 창의 좌표는 여러분이 직접 캡처한 그 사진과 같은 식으로 계산되기 때문에 좌표가 달리 계산될 일은 없습니다.

 

못 믿으시겠다고요? Windows Spy를 켜고 Windows 10에서 메모장을 켠 뒤, 왼쪽 테두리보다 아주아주 살짝 더 왼쪽 위치에 마우스를 위치시킨 후에 Window(Relative, Defult) 좌표를 읽어보세요. 분명 마우스는 창보다 왼쪽에 있는데, X 좌표는 음수가 아닌 것을 볼 수 있습니다. 이는 여러분이 실제 보시는 화면(왼쪽 사진)이 아닌, 가상으로 생성된 창(오른쪽 사진)을 기준으로 좌표를 계산하기 때문입니다. 좌표 작업을 하실 때 이런 차이는 신경을 안쓰셔도 된다는 뜻입니다.

왼쪽: 실제 윈도우 / 오른쪽: GDI+를 이용해 캡처


 이번 강에서의 gdip 함수 

Gdip_BitmapFromHWND()

매개 변수: 핸들 값
반환 값: 받은 핸들 값에서 가져온 비트맵의 메모리 주소