윈도 GUI 환경에서 동작하는 프로그램이 자기 창을 띄우기 위해 먼저 해야 하는 일은 바로 자기 윈도우의 클래스를 운영체제에다 등록하는 것이다. WNDCLASS 구조체와 RegisterClass함수는 그야말로 기본 중의 기본 필수 과정이다.
WNDCLASS 구조체에서 중요한 멤버는 클래스 이름(lpszClassName), 윈도우 프로시저 주소(lpfnWndProc) 정도다. 나머지 값들은 전부 0 / NULL이어도 클래스 등록이 가능하다.
우리에게 친숙한 에디트 컨트롤, 리스트박스, 콤보박스 등등은 다 고유한 클래스 이름이 존재하기 때문에 사용자 프로그램이 이를 변경하거나 없앨 수 없다. 윈도우 클래스계의 일종의 예약어라 해도 과언이 아니다. 공용 컨트롤은 내장 컨트롤 급의 붙박이는 아니지만 그래도 공용 컨트롤 매니페스트를 사용하는 요즘 프로그램들에서는 사실상 붙박이다.
대화상자, 메뉴, Alt+Tab 전환 창처럼 운영체제가 내부적으로만 사용하는 known 윈도우들도 사실은 다 고유한 클래스 이름을 갖고 있다.
한편, 각각의 윈도우 클래스 명부는 고유한 기억장소를 갖고 custom 데이터를 보관할 수 있다(cbClsExtra). 그러나 이건 거의 필요하지 않으며 쓰이지 않는다. 여러 윈도우 클래스들이 한 윈도우 프로시저를 공유하면서 그 프로시저가 클래스별로 custom 데이터를 가려서 동작하기라도 하지 않는 이상 말이다. 그런 게 아니라 윈도우 클래스별로 완전히 따로 노는 공유 데이터라면 그냥 해당 프로그램이 자체적으로 static/전역 변수의 형태로 갖고 있으면 될 일이다.
차라리 클래스가 아니라 각각의 윈도우들이 custom 데이터를 저장할 공간이라면(cbWndExtra) 이건 그래도 종종 쓰이는 경우가 있다. 그러나 굳이 이게 0이더라도 포인터 하나 정도 집어넣을 공간은 모든 윈도우들이 기본으로 갖고 있기 때문에 HWND로부터 그 창에 대응하는 C++ 객체의 포인터를 저장하는 것 정도는 이런 방식으로 하면 된다.
그 다음으로 외형 관련 부가 정보들은 나중에 클래스 차원이 아닌 윈도우 차원에서 변경 가능한 것들이다.
프로시저의 주소만 있으면 충분할 텐데 굳이 인스턴스 핸들까지 따로 받는 건 16비트 시절의 잔재이긴 하다. 요즘 같으면 윈도우 프로시저 주소가 어느 영역에 있는지만 봐도 이 윈도우 클래스의 소속 모듈은 곧바로 알 수 있으니 굳이 그 핸들을 따로 줄 필요는 없기 때문이다.
하지만 호환성 문제도 있고, 또 외형 리소스(메뉴, 마우스 포인터, 아이콘 등)를 어디서 불러올지 기준으로 삼을 모듈이 필요하기도 하니 인스턴스 핸들을 받는 란이 있는 것이다.
아이콘(hIcon)은 시스템 메뉴와 두꺼운 프레임이 갖춰진 커다란 윈도우를 만들 때에나 필요할 텐데,
여기서 지정한 뒤에도 나중에 실행 중에 WM_SETICON 메시지를 운영체제에다 보내서 변경이 가능하다. 대화상자의 아이콘을 바꿀 때 주로 쓰인다.
text를 바꾸는 것과는 달리 아이콘을 변경하는 건 함수가 전혀 존재하지 않고 메시지만 쓰인다는 게 특징이다.
또한, HICON 자체를 여러 크기의 아이템 컬렉션/패밀리로 설정한 게 아니라 특정 크기의 그림 하나만을 나타내게 설정한 바람에 WNDCLASS에 이어 WNDCLASSEX까지 등장하는 등 API가 다소 지저분해진 건 아쉬운 점이다. 지금은 이분법적인 큰 아이콘/작은 아이콘뿐만이 아니라 다양한 크기의 아이콘까지 등장해 있는데 말이다.
마우스 포인터(hCursor)는 잘 알다시피 WM_SETCURSOR 메시지가 왔을 때 동적으로 변경 가능하다.
기본 배경색(hbrBackground)으로 화면을 지우는 동작도 WM_ERASEBKGND 메시지 때 변경 가능하다.
즉, WNDCLASS에 지정된 것만이 절대적이지는 않다는 뜻이다.
그것도 모자라서 클래스 구조체에 메뉴(lpszMenuName)까지 지정 가능한 것이 굉장히 뜻밖이다.
보통 윈도우를 만들 때 메뉴 정보는 CreateWindowEx 함수에다가 따로 지정해 주기 때문이다. 그렇기 때문에 WNDCLASS 구조체에 굳이 메뉴 핸들이 공급될 필요는 없다.
본인 역시 10여 년간 Windows API로 프로그래밍을 하면서 이 멤버에다가 값을 지정해 준 적은 한 번도 없었다.
자, 그럼 이제 스타일(style)만 남는데, 다음과 같은 것들이 있다. 이 역시 굳이 이 스타일을 안 줘도 동일한 기능을 코드를 통해 얼마든지 재연할 수 있는 게 대부분이고, 오늘날에는 거의 필요하지 않거나 사용이 권장되지 않는 잉여 옵션도 있다.
1. 정말 유의미한 차이가 있음 (유일!): CS_GLOBALCLASS
원래 윈도우 클래스 명칭은 해당 클래스를 등록한 스레드도 아니고 그 등록한 코드가 들어있는 모듈(EXE든 DLL이든)에서만 쓸 수 있다. 그러나 이 옵션이 지정된 채로 등록된 윈도우 클래스는 해당 프로세스 전체에서 사용할 수 있게 된다.
어떤 특수한 윈도우--custom 컨트롤이 대표적인 예--에 대한 코드가 DLL에 들어있고 그 윈도우를 그 DLL을 불러들인 EXE에서 사용하고자 한다면, 그 클래스는 당연히 이 스타일이 지정된 채로 등록되어야 한다.
쉽게 말해 이 윈도우 클래스를 작성하지 않은 다른 EXE/DLL에서 컴포넌트처럼 생성되고 사용되고자 하는 윈도우라면 이 스타일이 반드시 필요하고, 그냥 한 프로그램 모듈 안에서 내부적으로만 사용하고 말 local 윈도우라면 지정하지 않으면 된다.
16비트 시절에는 이 스타일의 여파가 훨씬 더 강력해서 한 EXE가 등록해 놓은 윈도우 클래스를 다른 EXE가 마음대로 사용할 수도 있었다. 인스턴스 핸들로 데이터 세그먼트를 구분하는 게 오늘날로 치면 그냥 응용 프로그램의 주소 공간을 마음대로 넘나드는 거나 마찬가지였기 때문이다. 그러나 오늘날은 그렇게까지는 할 수 없으며, 일반적으로는 DLL에다가 윈도우 프로시저를 구현한 뒤, 그 윈도우를 사용하고자 하는 EXE가 DLL을 매번 불러오고 클래스 등록을 저렇게 해 줘야 한다.
2. 다른 코드를 통해 대체 가능한 동작 방식의 차이
CS_DBLCLKS
좌든 우든 한 마우스 버튼을 충분한 시간 간격 이내에 빠르게 연타했을 때, 둘째 클릭은 WM_?BUTTONDOWN이 아니라 WM_?BUTTONDBLCLK라는 메시지로 달리 알리게 한다. 이것은 실행 시간에 매번 바뀔 만한 동작 방식은 아니니 윈도우의 스타일이 아니라 클래스의 스타일로 존재하는 게 적절하긴 하다.
굳이 이 스타일이 없어도 더블클릭을 인식하는 것을 우리가 직접 구현하는 건 어렵지 않다. 그러나 타이머 체크를 해야 하고 예전 클릭 시점을 저장해 놓는 등 별도의 시간· 공간 오버헤드가 필요하기 때문에 운영체제는 이걸 원하는 윈도우에다가만 더블클릭 메시지를 전해 주고 있다. 더블로 모자라서 트리플 클릭이라도 인식하려면 역시 사용자가 상태 전환 로직을 직접 구현하는 게 필수일 게다.
메뉴의 경우 마우스의 클릭에 따라 열렸다가 닫히는 게 토글되는 물건이다. 더블클릭에 따른 동작 구분이 필요하지는 않지만, 보통은 더블클릭 때는 열렸던 메뉴가 닫히지 않게 만들어져 있다. 지금 당장 XP~7의 운영체제의 시작 메뉴를 눌러 보시기 바란다. 초보자들은 더블클릭을 할 필요가 없는 물건도 불필요하게 더블클릭하는 경향이 있기 때문에 더블클릭은 클릭+클릭으로 인식하지 않고 메뉴를 닫는 동작으로 인식하지 않는다.
물론 이런 정책은 진짜로 시도 때도 없이 짧은 간격의 클릭 연타를 인식해야 하는 게임 같은 데서는 절대로 적용해서는 안 될 것이다. 정반대의 정책을 취해야 한다.
CS_VREDRAW, CS_HREDRAW
일반적으로 창의 크기가 예전보다 커지면 커져서 새로 생긴 오른쪽 내지 아래쪽의 신규 영역에 대해서만 WM_PAINT가 날아온다. 그러나 이 옵션이 적용되면 가로 and/or 세로 크기가 바뀌었을 때 창 전체가 갱신되고 WM_PAINT가 날아온다.
화면에 문자열이 가로 내지 세로 기준으로 중앙 정렬되어 출력된다거나, 화면 폭에 따라 자동 줄바꿈이 적용되어 출력되고 있다면 화면 크기가 바뀌었을 때 화면 전체가 갱신되어야 할 것이다. 동일 배율의 2차원 비트맵을 찍는 경우가 아닌 이상, 화면 전체의 갱신이 필요한 상황은 생각보다 많다.
하지만 그럼에도 불구하고 이 옵션은 생각만치 그렇게 독창적이거나 유용하지 않다. WM_SIZE 메시지가 왔을 때 InvalidateRect를 수동으로 호출하는 것만으로도 동일한 효과를 낼 수 있기 때문이다.
CS_NOCLOSE
바로 얼마 전에 쓴 글에서 다루었듯이, 창에 [X] 버튼과 Alt+F4를 사용할 수 없게 만든다.
그러나 이것은 (1) GetSystemMenu를 이용해서 시스템 메뉴에 있는 '닫기' 명령을 없애거나 disable시키고, (2) WM_CLOSE 메시지가 왔을 때 이를 무시하여 DefWindowProc에다 전달하지 않으면 클래스 스타일 없이도 역시 거의 똑같은 효과를 얻을 수 있다.
3. 외형/성능과 관련된 마이너한 차이
CS_SAVEBITS
창이 생겼을 때 우리 창이 가리고 있는 배경 영역을 저장해 둔다. 그리고 우리 창이 사라지면 아래의 가려졌던 창에다가 WM_PAINT를 보내는 게 아니라 그냥 저장된 놈을 도로 뿌려 준다. 언제나 무조건 그렇게 하라는 뜻이 아니며 어지간한 비디오 메모리가 남아 있고 할 만하다 싶을 때만 그렇게 하라는 권장 사항이다.
이 스타일을 사용하는 윈도우는 크기가 작고 생성된 후에 이동하지 않으며, 잠깐 동안만 존재했다가 곧 없어지는 휘발성 강한 용도인 게 바람직하다. 툴팁, 메뉴 같은 윈도우의 클래스에 이 옵션이 지정돼 있다. 우리 코드의 동작 방식을 바꾸는 스타일이 아니기 때문에 존재감이 적다.
Windows Vista와 7의 Aero에서는 어차피 모든 창들의 내용이 메모리에 따로 저장되어 있고 DWM에 의해 합성되어서 출력되기 때문에 이 스타일의 존재감이 없는 거나 마찬가지다. 큼직한 창이 가려졌다가 다시 나왔는데도 예전 내용이 알아서 자동으로 출력되지 WM_PAINT가 오지 않는다니..! Vista가 출시되었을 때, Windows의 역사상 최초로 벌어지는 광경에 놀란 개발자들이 많았을 것이다.
CS_DROPSHADOW
Windows XP에서 최초로 도입된 이 스타일은 창 주변에 은은한 그림자 효과를 넣는다. Windows 2000에서는 마우스 포인터의 주변에 은은한 그림자를 넣는 효과가 추가되었는데, 동일한 알고리즘이 이제 임의의 창에도 적용된 것이다.
용도면에서 앞의 CS_SAVEBITS와 비슷한 구석이 있는지라, Windows XP부터는 툴팁과 메뉴에 이 스타일이 적용돼 있다.
시스템 메뉴와 뼈대가 갖춰진 일반적인 창에도 적용을 못 하는 건 아니지만, Aero 환경에서는 어차피 자체적으로 창 테두리 주변에 큼직한 그림자 효과가 추가되어 있기 때문에 이 클래스 스타일이 딱히 유효하지 않다.
또한 Aero 없는 모드에서는 그림자가 붙은 커다란 창은, 움직이거나 크기를 조절할 때 화면을 다시 칠하는 부담이 굉장히 커진다.
4. DC의 생성 방식과 관련된 이상한 옵션
Windows에서는 GDI API를 이용하여 화면에다 그림을 그리려면 먼저 device context라고 불리는 DC 핸들을 얻어 와야 하는 게 정석이다. 보통은 WM_PAINT 메시지가 왔을 때 BeginPaint 함수를 이용하여 얻으면 되는데, 다른 상황에서도 GetDC를 호출해서 얻을 수 있긴 하다. 그러나 BeginPaint는 딱 정확하게 칠해야 하는 영역에만 클리핑 영역이 최적화된 DC를 넘겨 주기 때문에 성능을 생각한다면 전자만을 이용하는 게 더 좋다.
윈도우와 이 DC 사이의 대응 관계는 생각보다 미묘하다. 일반적으로 시스템에 존재하는 창의 개수보다는 운영체제가 관리하는 화면용 DC의 개수가 더 적다. 솔직히 어떤 창이든 하드웨어 차원에서는 동일· 단일한 비디오 메모리에다 출력되는 것이니 동시 요청이 아닌 이상 DC가 굳이 많이 있어야 할 필요가 없다. 이 DC는 그때 그때 내부 상태가 초기화되고 클리핑 영역만 바뀐 채 재활용된다.
그런데.. CS_OWNDC가 지정되면 이 스타일이 적용된 클래스의 모든 창별로 별도의 전용 DC가 할당된다. 이는 프로그래밍 패러다임을 크게 바꿔 놓는다.
GetDC를 아무리 여러 번 호출해도, 그리고 BeginPaint를 호출해도 돌아오는 화면용 DC 핸들은 동일하며, 이 DC는 다른 윈도우에서는 쓰이지 않는다.
이 DC는 생명 주기가 자기가 소속된 윈도우와 동일하다. 그렇기 때문에 그림을 그려 준 뒤 GetDC 다음에 ReleaseDC를 하지 않아도 된다. 그리고 한 WM_PAINT 타이밍 때 지정했던 내부 상태가 다음 WM_PAINT때도 고스란히 보존되어 있다. 글자색, current positon, 선택되어 있는 GDI 개체들이 모조리..
통상적으로 해야 하는 초기화나 뒷정리가 필요하지 않으니 일면 편리한 점도 있지만, 이것은 생각보다 그리 큰 장점이 아니다.
그에 반해 윈도우 하나가 생길 때마다 수백 바이트에 달하는 전용 DC가 추가로 생성되는 것은 운영체제의 입장에서는 상당한 부담이었다. 특히나 16비트 시절에는 리소스라고 불리던 GDI 힙의 크기가 겨우 64K밖에 안 됐는데 이건 그야말로 리소스 잡아먹는 하마나 마찬가지였으며 심지어 윈도 9x에서도 상황이 크게 나아지지 않았다.
이 때문에 이 옵션은 존재는 하되 사용이 절대로 권장되지 않는 물건으로 전락했다.
CS_CLASSDC는 CS_OWNDC보다 메모리를 좀 아껴 보자는 발상에서 유래되었는데, 한 전용 DC를 동일 클래스에 소속된 모든 윈도우들이 공유하는 방식이다. 한 윈도우가 글자색을 빨간색으로 바꿔 놓으면, 다음에 그려지는 같은 윈도우는 기본적으로 글자가 빨간색으로 찍힌다. 이것도 기괴한 사고방식이긴 하다.
요건 GDI 자원을 좀 아낄 수 있을지는 모르나 '전용 DC'라는 장점이 사라지는 상태에서 멀티스레드 환경에서는 상당히 위험한 결과를 초래할 가능성이 있기 때문에 32비트 이후에서는 단점만 더욱 부각되었으며, 역시 봉인된 옵션으로 전락했다. 이런 게 있다는 것 정도만 알면 된다.
다음으로 CS_PARENTDC는 위의 두 옵션과는 성격이 약간 다르고 약간 더 실용성이 있다. 자기를 그릴 때 부모 윈도우의 기존 DC를 활용해도 좋다고 알려 준다. 좀 더 구체적으로 말하자면 굳이 클리핑 영역을 자기 윈도우로 맞추지 않아도 된다고 알려 준다.
대화상자에 아기자기한 컨트롤이 굉장히 많이 있는데 그 컨트롤들이 CS_PARENTDC 스타일이 맞춰져 있다면 대화상자의 그리기 속도가 약간이나마 향상될 수 있다. 자신이 클리핑 기능 없이도 알아서 자기 클라이언트 영역을 안 벗어나고 똑똑하게 그림을 그릴 자신이 있다면 이 스타일을 사용하는 게 나쁘지 않으며, 실제로 운영체제의 표준 컨트롤들은 다른 잉여스러운 옵션 말고 이 옵션은 사용한다고 한다.
다만, 이런 꼼수를 허용하는 스타일이 존재하는 경우, layered 윈도우나 drop shadow 같은 특수 효과와는 충돌이 발생할 수 있으니 사용 전에 MSDN 설명을 참고하는 게 좋다. 요즘 같이 메모리와 성능이 풍족한 시대엔 그냥 저런 기괴한 옵션들은 다 잊어버리고 그냥 0으로만 지정해도 무관하다.
5. Windows 1~2.x 시절에나 유효하던 완전 캐잉여: CS_BYTEALIGNCLIENT, CS_BYTEALIGNWINDOW
이것은.. 거의 20년 이상 전부터 전혀 쓸모가 없어진 옛날 잔재이다.
잘 알다시피 옛날에는 컴퓨터 화면이 흑백이던 시절이 있었고 이때는 1바이트가 8비트, 즉 8개의 가로 픽셀을 담당했었다.
그러니 이 스타일은 창의 꼭지점 위치(외곽 모서리 또는 클라이언트 모서리 기준)를 강제로 8 또는 최소한 4의 배수 위치로 맞춰서 그림을 찍는 게 바이트 경계에 딱 걸쳐지는 게 보장되게 하는 역할을 했다.
당연히 256컬러, 혹은 16컬러가 지원된 시점부터 모든 픽셀은 자동으로 이미 바이트 align이 맞춰지기 시작했으며 이 옵션은 전혀 필요가 없어졌다.
또한 Windows 3.0부터는 영문이 가변폭 글꼴로 출력되기 시작한지라 그렇잖아도 글자를 찍을 때 어차피 바이트 align이 무의미해지기도 했다.
지금쯤이면 이 스타일이 갖던 값은 그냥 다른 용도로 재사용해 버려도 되지 않나 싶다. 하지만 extended 스타일까지 존재하는 윈도우와는 달리, 클래스에는 스타일이 그렇게 다양하게 많이 추가될 여지도 별로 없긴 하다.
Windows API를 심층 연구하는 건 재미있다. ^^;;
Posted by 사무엘