Windows 운영체제에서 GUI의 핵심 구성 요소인 윈도우라는 물건은 자신만의 위치, 스타일, 소속 클래스, 부모 윈도우, ID(또는 메뉴 핸들) 등 여러가지 공통 정보를 갖는다. 이들은 숫자 형태로 표현되며, 대부분 GetWindowLongPtr 함수를 이용해 값을 얻을 수 있다.
그런데 그 공통 정보 중에는 숫자뿐만 아니라 문자열인 것도 있으니, 바로 '텍스트'이다. static 컨트롤이나 버튼들이 출력하고 있는 문장이나 단어들이 바로 자신이 갖고 있는 텍스트이다. 그리고 작업 표시줄에 걸려 있는 응용 프로그램들의 제목(Caption)도 다 윈도우의 내부 텍스트이다.
윈도우에 소속된 텍스트를 읽고 쓰는 함수는 잘 알다시피 Get/SetWindowText이다. 그러나 이것의 실제 구현은 그저 C++ 클래스에서
String getText() const { return m_strText; }
void setText(String t) { m_strText=t; }
요렇게 달랑 코딩하는 것만치 단순하지 않으며, 그보다 추상화 계층이 많고 사연이 훨씬 더 복잡하다.
(1) 먼저, 윈도우는 단순히 자신의 내부 버퍼를 토대로 텍스트를 지정하거나 되돌릴지, 아니면 이런 기본 동작을 무시하고 매번 자신이 능동적으로 반응을 할지를 자기 윈도우 프로시저가 재량껏 결정할 수 있다. 이것은 마치 WM_PAINT 메시지를 이미 저장돼 있는 버퍼를 바로 뿌리는 것으로 처리하는지, 아니면 매번 그래픽 API로 그려서 처리하는지의 차이와 같다.
아까도 얘기했던 static이나 버튼 컨트롤을 포함해 대부분의 윈도우들은 기본 내부 버퍼를 기반으로 동작하는 것만으로도 충분할 것이다. 그러나 에디트 컨트롤은 get의 경우 사용자가 입력한 문자열을 되돌리며 set은 자기가 편집하고 있는 텍스트를 변경한다. 이 텍스트는 당연히 기본 내부 버퍼가 아니라 에디트 컨트롤이 자체적으로 관리하는 자료 구조로부터 얻어진 데이터이다.
그럼 그런 customize를 어떻게 하는가? Get/SetWindowText는 대상 윈도우에다가 WM_GETTEXT, WM_GETTEXTLEN, WM_SETTEXT 메시지를 보내어 그 응답을 받는 방식으로 구현된다. 윈도우가 이 메시지를 처리하지 않고 그냥 DefWindowProc으로 넘기면 내부 버퍼에 대한 문자열 get/set인 기본 기능이 구현된다. 이 메시지는 자기가 에디트 컨트롤을 직접 구현하고 있을 때 정도에나 직접 처리하면 된다.
WM_CREATE에서 넘어온 CREATESTRUCT 구조체를 들여다보면, 이 윈도우를 생성한 CreateWindowEx에서 지정해 준 window name 정보가 있는데, 이것부터 자기 자료 구조에다가 복사를 해 둬야 할 것이다.
만약 내 윈도우가 대용량 텍스트를 다루고 텍스트의 일부 구간이 빈번하게 수정된다면, 저런 원시적인 메시지에만 의존하지 말고 해당 동작만을 위한 custom 메시지를 직접 구현해서 그걸 이용하는 게 효율적이다.
(2) 서로 다른 응용 프로그램끼리는 너무 당연한 말이지만 포인터를 주고받을 수 없다. 서로 메모리 주소 체계가 완전히 다르며, 상대방 프로세스의 메모리 내부를 들여다볼 수 없기 때문이다. 그렇기 때문에 이런 프로그램들끼리 가변 길이의 임의의 데이터를 주고 받으려면 WM_COPYDATA라는 특수한 메시지를 써야 하는데, 이건 의외로 굉장히 유용하고 괜찮은 물건이긴 하다.
그런데 WM_GET/SETTEXT 메시지도 비슷한 구석이 있다. 서로 다른 응용 프로그램간에도 마치 동일 프로그램인 것처럼 텍스트를 얻어 오거나 지정할 수 있다. 이 메시지에 대해서도 운영체제가 예외적인 변환 처리를 해 주기 때문이다.
단, 여기에서는 Get/SetWindowText 함수는 동작에서 약간 차이를 보인다. 같은 프로세스끼리 get/set을 하는 것은 메시지를 날리는 것과 동일하게 동작하는 반면, 다른 프로세스에 있는 윈도우에다가 get/set을 하면.. 그 윈도우의 윈도우 프로시저를 실행하는 게 아니라 그냥 그 윈도우에 있는 내장 버퍼의 문자열만 가져오는 걸로 끝난다. 왜 그렇게 된 걸까?
이것은 보안을 위해서이다. 해당 윈도우를 관할하는 응용 프로그램이 현재 메시지에 반응하지 않고 뻗어 있는데(hang) 이때 또 텍스트를 얻거나 지정하는 메시지를 보내면.. 그 요청을 한 우리 응용 프로그램까지 응답을 기다리느라 뻗어 버릴 수 있기 때문이다. 그래서 이때는 언제나 실행이 정상적으로 즉시 끝나는 게 보장되는 윈도우 내부 버퍼만을 건드리는 걸로 끝난다.
단순 내부 텍스트가 아니라 위험을 감수하고라도 해당 프로그램의 윈도우 프로시저가 실시간으로 실행한 텍스트를 얻고 싶으면, 함수만 달랑 호출하지 말고 직접 WM_GETTEXT 메시지를 보내라는 것이 설계 의도이다. 뻗을 걱정을 최소화하면서 메시지를 보내는 함수로는 SendMessageCallback이라든가 SendMessageTimeout 같은 물건들도 있으니 말이다.
사실, 현재 시스템에 떠 있는 모든 윈도우들을 조회하는 Spy++ 같은 급의 특수한 프로그램을 만드는 게 아닌 이상, 다른 프로그램들에서 제일 겉의 프레임 윈도우도 아니고 내부의 에디트 컨트롤까지 시시콜콜하게 내용을 다 들여다봐야 할 일은 거의 없을 것이다. 그러니 16비트 API에서 최초로 32비트 멀티스레드 환경으로 넘어갈 때, 텍스트를 얻는 동작은 함수 호출에 대해서만 저런 조치를 취하는 것이 적절한 조치라고 마소의 엔지니어들이 판단했다.
(3) 프로세스 장벽에 비해서는 아주 사소한 문제에 불과하지만, 사실은 문자열 인코딩 장벽도 있다.
어떤 윈도우가 있어서 요즘 추세대로 유니코드를 사용하며 WM_GETTEXT에 대해서는 L"abc" 같은 wide 문자열만 되돌린다. 그런 윈도우를 대상으로 GetWindowTextA 함수를 호출했다 하더라도 문자열 변환은 운영체제가 몰래 알아서 해 준다. 해당 프로그램은 ansi 문자열인 "abc"를 받는다.
요청하는 측에서는 GetWindowText나 SendMessage를 A와 W 중 어느 버전으로 호출했는지로 판단을 하고, 받는 윈도우는 RegisterClass(Ex)에서 A와 W 중 어느 버전을 이용해서 등록했는지로 판단한다.
그럼, 텍스트 지정 API와 관계가 있는 다른 분야 얘기를 또 둘 늘어놓고 글을 맺도록 하겠다.
부록 1. 창을 없애는 것과의 관계
윈도우의 텍스트를 얻어 올 때 함수를 쓰느냐 메시지를 보내느냐 하는 건, 마치 윈도우를 닫을 때 직통으로 DestroyWindow를 호출하느냐 아니면 WM_CLOSE 메시지를 보내느냐 하는 것과 비슷한 구석이 있어 보인다.
WM_CLOSE는 마우스로 X 버튼 누르거나 키보드로 Alt+F4를 누른 것과 같다. 이 메시지를 더 처리하지 않고 DefWindowProc으로 넘기면 얘가 DestroyWindow를 호출해 준다. 응용 프로그램의 경우 "이 문서를 저장하시겠습니까?"라고 질문 메시지를 출력하는 게 이 메시지를 받았을 때이다. 물론 '취소'를 누르고 메시지를 씹으면 실제로 닫히지는 않는다.
그 반면, WM_DESTROY는 이미 자기가 닫히고 없어지는 건 막을 수 없는 지경이 됐고, 그 전에 마무리 작업이나 하라는 통지이다. 아직 자기의 자식 윈도우들도 다 남아 있다. 이 메시지는 운영체제로부터 자연스럽게 받게 해야지 사용자가 인위로 생성하지는 말아야 한다.
이게 끝나고 자식 윈도우까지 다 사라진 뒤에 정말 마지막으로 오는 메시지가 바로 WM_NCDESTROY이다. 이때 하는 일은 C++ 클래스와 연결된 윈도우 프로시저에서 delete this를 하는 것 정도가 전부이다.
Get/SetWindowText가 여타 프로세스의 윈도우에 대해서는 다소 방어적으로 동작을 하듯, DestroyWindow에도 약간의 방어적인 제약이 있다. MSDN의 설명에 따르면, 얘는 다른 프로세스 정도가 아니라 아예 다른 스레드에 의해 생성된 윈도우를 파괴하지는 못한다. 그런 창을 닫으려면 그냥 WM_CLOSE라는 간접적인 방법만을 써야 한다.
앞서 말했듯이 WM_CLOSE는 응용 프로그램이 회피· 거부가 가능하기 때문에 응용 프로그램의 입장에서는 다른 스레드/프로세스의 특정 윈도우를 무조건적으로 없애지는 못한다. 굳이 그렇게 해야 할 필요도 없을 테고.
멀티스레드 환경에서 Windows의 창 관리자가 어떤 식으로 API를 설계했는지를 살펴보면 편의와 안정성을 상호 절충하기 위해 여러가지 생각을 했다는 점을 발견할 수 있다.
부록 2. 아이콘과 글꼴은 어떻게?
앞서 살펴본 것처럼 한 윈도우의 속성 중에 텍스트(TEXT)는 운영체제가 DefWindowProc을 통해 내부 텍스트를 관리하기도 하고 한편으로 값을 읽고 쓰는 동작을 customize하는 방법도 제공한다.
그럼, 윈도우와 관련된 부가 정보 중에는 아이콘과 글꼴은 어떻게 관리되는 걸까? 얘들은 함수가 아니라 메시지로만 값을 지정하고 얻어 온다는 공통점도 있다. WM_(GET/SET)(ICON/FONT)라고 말이다.
아이콘은 기본적으로 윈도우가 클래스에 소속돼 있으며, 그 클래스를 기반으로 만들어진 윈도우들이 한 아이콘을 공유하는 형태이다. 그러나 클래스 아이콘과는 별개로 필요하다면 각각의 윈도우도 자기 아이콘을 예외적으로 변경할 수 있다. 이는 특정 윈도우 클래스가 아니라 운영체제 차원에서 기본으로 제공되는 기능이다.
코드로 표현하자면 대충 이런 구조. commonIcon뿐만 아니라 myIcon도 있다는 뜻이다.
class Window {
static Icon commonIcon;
Icon myIcon;
public:
Window() { myIcon=commonIcon; }
Icon getIcon() { return myIcon; }
void setIcon(Icon i) { myIcon=i; }
};
그렇기 때문에 대화상자는 윈도우 클래스는 동일하지만 응용 프로그램이 WM_SETICON을 보냄으로써 자신만의 아이콘으로 customize를 할 수가 있다. 그리고 이 기능은 대화상자에만 있는 게 아니라 임의의 top-level 윈도우가 다 갖추고 있다.
이런 아이콘에 비해 글꼴은 운영체제의 윈도우 내부 자료구조에 정보가 자동으로 저장되지 않는다. 내가 custom 컨트롤을 만들고 있고 거기에 대화상자의 기본 글꼴대로 문자를 찍는 기능이 있다면 내부적으로 HFONT 핸들을 멤버로 추가하고 WM_GET/SETFONT 메시지를 직접 구현해 줘야 한다. 즉, 운영체제에는 인터페이스만 정의되어 있을 뿐이다.
요약하자면 text는 내부 자료구조와 custom 동작이 모두 존재하고, icon은 내부 기본 동작만으로 충분하고, font는 기본 동작이 없기 때문에 필요한 경우 자체 구현을 해야 한다는 뜻이다.
Posted by 사무엘