Windows 운영체제에서 GUI 요소로서 화면에 표시되는 윈도우들은 독자적으로 고유한 위치와 크기를 갖는 popup 및 overlapped 윈도우, 아니면 다른 창의 내부에 부속으로 딸려 있는 child 윈도우라는 두 형태로 나뉜다. 전자는 독립 윈도우이고 후자는 종속 윈도우라고 생각해도 될 것 같다.

그리고 전자는 운영체제가 자동으로 표시해 주는 제목 표시줄, 시스템 메뉴, 최소화/최대화 버튼 같은 걸 가질 수 있고 메뉴도 가질 수 있다. 물론 갖지 않는 것도 자유이며 해당 창의 재량이다.
그럼 반대로 후자는 사정이 어떨까? 차일드 윈도우가 자기가 속한 부모 윈도우 내부에서 또 제목 표시줄과 시스템 메뉴, 최소화/최대화 버튼 따위를 가질 수 있을까?

이에 대해서 결론부터, 답부터 말하자면 다음과 같다.
"외형만 그렇게 나오도록 할 수는 있다. 하지만 운영체제는 child 윈도우를 대상으로는 여느 popup/overlapped 윈도우에서 수행하는 기본 처리를 자동으로 해 주지는 않는다. 그렇기 때문에 창을 그렇게 만들어 놓은 뒤에 우리가 원하는 자연스러운 동작을 구현할 수는 없다. 이건 운영체제에서 의도한 보편적인(?) 방식대로 창을 만들고 활용하는 게 아니기 때문이다."

본인은 마소에서 보편적이지 않은 동작이 존재하는 프로그램을 만든 것을 지난 20여 년에 달하는 세월 동안 딱 하나 발견했다.
바로 HTML 도움말 파일을 생성해 주는 HTML Help Workshop이라는 툴에 내장된 그래픽 에디터이다.

도움말을 띄우면 HTML 도움말 창이 별도의 독립된 창으로 뜨는 게 아니라 자신의 도킹 패널 내부에.. child 형태로 뜬다! 시스템 메뉴와 캡션, 최소/최대화 버튼까지 있는 당당한 독립 윈도우가 무슨 MDI child 윈도우처럼 다른 윈도우 내부에 이상하게 끼여 있는 게 심히 기괴하다. 이런 것도 Windows API를 사용해서 이론적으로 만들 수 있다는 것이다.

사용자 삽입 이미지

그래서 본인도 간단히 실험을 해 봤다.
다음은 껍데기 overlapped 윈도우와 "동일한" 클래스 이름으로 자기 안에 child 윈도우를 또 만들어 돌린 모습이다. 윈도우를 생성할 때 WS_CHILD에다가 WS_CAPTION| WS_SYSMENU| WS_MINIMIZEBOX| WS_MAXIMIZEBOX| WS_THICKFRAME만 주면 됐다.

사용자 삽입 이미지

얘는 언뜻 보기에 그냥 평범한 클래식 MDI 앱처럼 생겼지만 실제로는 그렇지 않다.
자신이 껍데기일 때는 클라이언트 영역을 회색 GetSysColor(COLOR_APPWORKSPACE)로 칠하고, 차일드일 때는 흰색 COLOR_WINDOW로 칠하면 된다. 껍데기와 차일드 둘 다 동일 윈도우 프로시저가 수행한 결과물이다. 자기 창이 껍데기인지 차일드인지의 여부는 WS_CHILD 스타일의 존재 여부만으로 간단히 판별할 수 있다.

그럼 제목 표시줄이 달린 차일드 윈도우들을 저렇게 만들어 놓으면 부모 윈도우라는 틀 안에 종속된 overlapped/popup 윈도우처럼 매끄럽게 동작하는가?
안타깝지만 답은 "전혀 그렇지 않다"이다. 저 상태에서 창들을 마우스로 단 몇 분 동안만 조작해 봐도 이상한 점이 잔뜩 발견될 것이다.

마우스로 child 윈도우를 클릭하면 지금 화면의 제일 겉에 드러난 창이 아니라 엉뚱한 창이 인식된다. 윈도우를 드래그 해 보면 화면에 잔상이 생긴다.
WM_LBUTTONDOWN 메시지를 받은 child 윈도우에다가 SetFocus를 해도 제목 표시줄이 활성화/비활성화 처리가 되지도 않는다.
child 윈도우를 최대화한 모습은 꽤 어정쩡하며, 그 상태로 프로그램 창의 크기를 키워도 child 윈도우는 크기가 갱신되지 않는다.

이런 문제가 발생하는 이유는.. Windows라는 운영체제는 근본적으로 child 윈도우에 대해서는 이들이 서로 겹쳐져 있고 포개져 있을 때의 Z-order 처리를 overlapped/popup 윈도우처럼 정교하게 해 주지 않기 때문이다.
child 윈도우라는 건 저렇게 제목 표시줄과 시스템 메뉴가 있는 윈도우가 아니라.. 그냥 리스트 컨트롤, 에디트 컨트롤, 버튼 같은 물건들이다. 에디트 컨트롤이나 버튼 같은 게 서로 겹쳐질 일이 있고 포커스를 받았을 때 Z-order가 바뀌어야 할 일이 도무지 있는가? 그렇지 않다는 것이다.

사용자 삽입 이미지

더구나 child 윈도우의 제목 표시줄은 Windows 8/10에서도 옛날 Windows Vista/7의 기본 테마 모양 그대로이며 업데이트도 안 돼 있다. 푸르스름하고 모서리가 둥근 그 시절 디자인 말이다. 그만큼 이쪽 외형은 마소에서도 관심이 없으며 더 지원을 안 하고 손을 놨다는 뜻이다.

저 상황에서 화면 잔상이 발생하는 걸 조금이라도 줄이려면 이 윈도우의 클래스에다가는 화면 refresh를 적극적으로 하라고 CS_HREDRAW|CS_VREDRAW 스타일을 줘야 하더라.
그리고 마우스 메시지를 받았을 때 SetWindowPos를 호출해서 이 창의 Z-order를.. HWND_BOTTOM으로 지정해야 하더라. 왜 top이 아니라 bottom인지는 모르겠다.

저것만 한다고 해서 모든 문제가 해결되는 건 아니다. 이런 시행착오를 겪어 보면.. MDI 프로그램에서 일개 child 윈도우인 문서창들이 나름 MDI client 영역 내부에서 껍데기 독립 윈도우인 것처럼 유연하게 처리되는 게, 절대로 그냥 저절로 되는 일이 아님을 짐작할 수 있다. 운영체제 API 차원에서 추가적인/예외적인 보정을 굉장히 많이 해 주는 덕분이지 싶다.

실제로 MDI를 구현하기 위해 사용되는 윈도우 프로시저는 DefFrameProc와 DefMDIChildProc로 감싸져 있으며, 키보드 전처리도 TranslateMDISysAccel로 따로 있다. 대화상자에 고유한 윈도우 프로시저와 키보드 전처리(IsDialogMessage)가 있는 것과 비슷한 맥락이다.

MDI 문서창들은 WS_EX_MDICHILD라는 extended 스타일이 지정돼 있다. 물론 우리야 CreateMDIWindow 함수나 WM_MDICREATE 메시지로 생성 요청만 하기 때문에 저 스타일을 직접 지정할 일은 없다.
내부적으로 뭔가 큰 의미를 갖는 스타일이지 싶은데.. 진짜 MDI 문서창이 아닌 임의의 child 윈도우를 생성할 때 저 스타일을 일부러 줘 보면 CreateWindowEx 함수의 실행이 실패한다. 그러니 쟤는 비록 헤더 파일에 선언은 돼 있지만 사용법과 의미가 제대로 문서화되지 않은 내부 API나 마찬가지이다.

그 밖에 WS_CLIPCHILDREN이라든가 WS_CLIPSIBLINGS, WS_EX_TRANSPARENT는 child 윈도우가 영역이 여럿 겹쳐 있을 때 창들을 refresh 하는 방식이나 성능을 좌우하는 옵션일 텐데 본인은 구체적인 의미나 차이를 잘 모르겠다. 평소에는 몰라도 전혀 상관 없지만 child 윈도우들을 MDI 문서창처럼 정교하게 다루기 위해서는 알아야 할 기능이지 싶다.
이런 창을 마우스로 클릭하면 WM_CHILDACTIVATE라는 메시지도 온다는데 본인은 태어나서 지금까지 한 번도 안 써 봤다. 저런 메시지가 있다는 사실도 처음 알게 됐다.

오늘 이야기의 결론은 MDI 형태의 프로그램을 밑바닥부터 직접 구현하는 건 대단히 어렵고 삽질스럽다는 것이다.. =_=;; child 윈도우를 popup/overlapped 윈도우처럼 다뤄 줘야 하기 때문이다.
그런 데다가 한 프로그램 창 안에서 여러 문서창을 다루는 것은 오늘날 같은 멀티 모니터 환경하고도 그리 어울리지 않는다.

그러니 마소 프로그램들의 경우, Word는 거의 20년 전부터 문서창을 프로그램 창처럼 따로 따로 생성하는 형태로 바뀌었으며 Visual Studio IDE도 문서창을 새 탭 아니면 새 창으로 자유자재로 만들 수 있게 바뀌었다.
그래도 그 정도보다는 규모가 작은 아담한 프로그램에게는 여전히 MDI만 한 대안이 없으니 클래식 레거시 MDI 기능도 오늘날까지 완전히 멸종하지는 않고 명맥을 유지하고 있다.

Posted by 사무엘

2020/06/20 08:35 2020/06/20 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1764

오늘날 마이크로소프트는 운영체제와 오피스뿐만 아니라 개발툴 분야도 세계를 석권해 있다.
걔들은 과거에 운영체제 쪽은 맥 내지 IBM OS/2와 경쟁했었고, 오피스는 로터스, 워드퍼펙, 한컴(...)과 경쟁했으며.. 개발툴 쪽은 볼랜드라는 쟁쟁한 기업과 경쟁했다.

마소와 볼랜드가 내놓았던 프로그램 개발툴은.. 먼저

1. IDE까지 있는 도스용 대중 보급형의 브랜드가 있었다.
볼랜드는 터보, 마소는 퀵.. 뭔가 스피디한 단어를 썼다는 공통점이 있다.
그리고 볼랜드는 브랜드명-언어명 사이를 띄었지만, 마소는 둘을 붙여 썼다.;;

Turbo Basic, Turbo C, Turbo Pascal
QuickBasic, QuickC, QuickPascal

다음은 볼랜드 말고 '마소'에서 개발했던 QuickC와 QuickPascal IDE의 스크린샷이다. 보기에 참 생소하다. 출처는 유명한 고전 소프트웨어 라이브러리인 WinWorld이다.

사용자 삽입 이미지

사용자 삽입 이미지

마소는 QuickBasic만 건지고 나머지는 다 망했다. QuickBasic이야.. 뭐 무료 축소판 QBasic을 MS-DOS와 Windows에다 포함시키기까지 했을 정도이고 말이다. 빌 게이츠가 베이식 언어를 아주 좋아했다.
그 반면 볼랜드는 Turbo Basic만 망하고 C와 Pascal을 건졌다. Turbo Basic의 개발진은 볼랜드를 퇴사하고 따로 회사를 차려 PowerBasic을 만들게 됐다.

2. 다음으로, 본가에 속하는 최상위 플래그십 제품군에는 그냥 자기 회사명을 붙였다.

Borland Pascal, C++
Microsoft Basic, C/C++

1990년대에 C에 이어 C++ 컴파일러가 개발되면서 자기 제품의 공식 명칭을 아예 C++이라고 바꿔 붙이는 곳이 있는가 하면, C와 겸용임을 내세우면서 C/C++이라고 붙이는 곳도 있었다.

볼랜드의 경우 C++을 C와는 완전 별개로 취급했는지 버전까지 1.0으로 도로 리셋하면서 Turbo C++ 내지 Borland C++이라고 작명했지만.. 마소는 C++을 기존 C 컴파일러의 연장선으로 보고 MS C 6.0 다음으로 7.0을 MS C/C++ 7.0이라고 작명했다. 사실, 연장선이라고 보는 게 더 일반적인 관행이었다.

참고로 왓콤 역시 Watcom C 9.0의 다음 버전이 Watcom C/C++ 9.5가 돼서 마소와 비슷하게 작명과 버전 넘버링을 했다. 왓콤은 제품이 짬이 길다는 인상을 주기 위해 첫 버전을 일부러 1이 아닌 6.0부터 시작하는 기행을 벌였었다! 볼랜드의 버전 넘버링과 비교하면 극과 극 그 자체였다.

터보 C++이랑 볼랜드 C++의 차이는.. 더 덩치 큰 상업용 프로그램 개발을 위한 OWL/Turbo Vision 같은 자체 프레임워크 라이브러리를 제공하느냐 여부 정도였지 싶다. 프로페셔널 에디션이냐 엔터프라이즈 에디션이냐의 차이처럼 말이다. 그리고 이때쯤 Windows용 지원도 시작됐다.

3. 그랬는데, 1990년대 이후부터는 그 플래그십 제품군도 Windows 전용의 더 고급 브랜드로 대체됐다.

볼랜드는 90년대 중반의 Delphi와 C++Builder로,
마소는 그 이름도 유명한 비주얼 브랜드로 말이다. Visual Basic, Visual C++.
그리고 마소도 Visual C++부터는 C/C++ 대신 C++만 내걸기 시작했으며,

관계가 이렇게 된다.
Visual C++이 과거 MS C/C++을 계승한 거라는 흔적은 _MSC_VER 매크로 값이 Visual Studio 자체의 버전보다 더 크다는 점을 통해서나 유추할 수 있다.

1이 2를 거쳐 3으로 바뀌는 동안 주변에서는 C 대신 C++이 대세가 되고, 주류 운영체제가 도스에서 Windows로 완전히 넘어가고 거대한 프레임워크 라이브러리가 등장하는 등의 큰 변화가 있었다. 개발 환경도 단순히 코딩용 텍스트 에디터와 디버거 수준을 넘어서 RAD까지 추구하는 수준으로 발전했다.

또한, 이 3단계가 주류가 될 즈음부터 마소의 Visual 툴들이 볼랜드를 완전히 꺾고 제압해 버렸다.
마소가 운영체제 홈그라운드라는 이점을 갖고 있기도 했거니와, 또 근본적으로는 파스칼이라는 언어 자체가 볼랜드의 창업자인 필립 칸이 선호하거나 예상한 것만치 프로그래밍계의 주류가 되지 못하고 마이너로 밀려난 것이 크게 작용했다. 네이티브 코드 생성이 가능하면서 빌드 속도가 왕창 빠른 건 개인적으로 무척 마음에 들었는데 말이다..;;

그에 반해 마소의 베이식은 파스칼보다 그리 나은 구석이 없는 언어임에도 불구하고 자사 운영체제의 닷넷빨 있지, 레거시 베이식도 자사 오피스의 VBA 매크로 언어가 있으니 망할 일이 없는 지위에 올라 있다.

한때(1990년대 후반??)는 파스칼이 언어 구조가 더 깔끔하고 좋다면서 정보 올림피아드 같은 데서라도 각광 받았지만.. 지금은 그런 것도 없다. 그 바닥조차도 닥치고 그냥 C/C++이다.
델파이를 기반으로 이미 만들어진 유틸리티나 각종 DB 연계 프로그램들(상점 매출 관리 등등..), SI 쪽 솔루션을 제외하면 파스칼은 마치 아래아한글만큼이나 입지가 좁아져 있지 않나 싶다..;;.

범언어적인 통합 개발 환경이라는 개념을 내놓은 것도 마소가 더 일렀다. Visual Studio가 나온 게 무려 1997년이니까.. 개발툴계의 '오피스'인 셈이다. (Word, Excel 등 통합처럼 Basic, C++ 통합). 그에 비해 볼랜드 진영에서 Delphi와 C++Builder를 통합한 RAD Studio를 내놓은 것은 그보다는 훨씬 나중의 일이다.

Windows NT야 이미 있던 16비트 Windows와 버전을 맞추기 위해서 3.1부터 시작했는데, Visual Studio의 경우, 공교롭게도 1990년대 중반까지 Visual Basic과 Visual C++의 버전이 모두 4.x대였다.
그래서 첫 버전인 Visual Studio 97은 각각의 툴 버전과 Studio 버전이 모두 깔끔하게 5로 맞춰졌으며, 이듬해에 나온 차기 버전은 어째 98이라는 연도 대신, 버전인 6으로 맞춰질 수 있었다.

2010년대 이후로 C++이 워낙 미친 듯이 바뀌고 발전하고 있으니.. D 같은 동급 경쟁 언어들조차 기세가 꺾이고 버로우 타는 중이다. 도대체 지난 2000년대에 C++98, C++03 시절에는 C++ 진영이 export 병크 삽질이나 벌이면서 왜 그렇게 침체돼 있었나 의아할 정도이다. 그 사이에 Java나 C# 같은 가상 머신 기반 언어들이 약진하니, 뭘 모르는 사람들은 겁도 없이 "C++은 이제 죽었네" 같은 소리를 태연히 늘어놓을 지경까지 갔었다. (2000년대 중반이 Windows XP에, IE6에... PC계가 전반적으로 좀 '고인물'스러운 분위기로 흘러가던 때였음) 한때 잠시 그러던 시절이 있었다.

Posted by 사무엘

2020/01/20 08:34 2020/01/20 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1707

요즘 Visual C++ 사용 메모

1. 디버깅 관련

수 년 전에 본인은 Windows에서 명령 프롬프트와 디버그 로그(OutputDebugString)에 유니코드가 지원되는 날이 언제쯤 올까 푸념을 늘어놓은 적이 있었는데.. 이건 놀랍게도 Windows 10에서 명령 프롬프트의 유니코드화(특수한 여건이 갖춰졌을 때 부분적으로 한해서)와 더불어 그럭저럭 현실이 됐다.

디버거 툴에 대해서 본인이 더 원하는 것은..

(1) IDE가 디버거를 붙여서 직접 실행해 준 디버기 말고.. 타 프로세스에 의해 실행된 디버기도 자동으로 감지해서 breakpoint 내지 로그 출력을 잡아 주기
(2) breakpoint의 작동 조건으로, "임의의 타 지점을 먼저 지나쳤거나 그게 call stack 아래에 있을 것" 정도 지정하기

정도이다.
(1)을 위해서 Attach to process 같은 기능이 이미 있긴 하다. 하지만 내 프로그램이 아주 잠깐 동안만 짤막하게 실행되고 마는 상황이라면(정상적인 종료이든, 오류로 인한 종료이든) 사용자가 느릿느릿 일일이 저 명령을 내릴 겨를이 없다.
이건 EXE의 디버깅도 DLL의 디버깅과 비슷한 양상으로 만든다. 실행 인자를 사용자가 지정해 주는 게 아니라, 이 EXE는 다른 EXE로부터 어떤 인자를 받아서 실행됐는지를 디버거로부터 안내받게 될 것이다.

(2)는 물론 코드 자체를 고쳐서 상태 변수 같은 걸 global하게 추가하는 식으로 편법으로 구현할 수는 있다. 하지만 그건 몹시 귀찮고 불편하다.
디버깅을 해야 하는 코드가 여러 부분에서 호출되고 있는데 우리는 특정 상황에서 호출된 것에만 관심이 가 있는 거.. 생각보다 자주 있는 일이다. 이에 대한 지원이 더 잘 된다면 프로그래머의 생산성이 많이 향상될 수 있을 것이다.

글쎄, 위의 두 아이템은 오래 전에 이미 언급한 적도 있을 것이다.
이것 말고.. 딱히 기술적으로 어려울 것 전혀 없는데 좀 있었으면 좋겠다 싶은 기능으로는..
디버깅을 위해 실행할 프로그램과 인자(argument)를 여러 세트 등록해 놓고.. 사용자가 예전에 등록해 놨던 세트를 곧장 불러올 수 있으면 좋겠다.

지금도 Debug 탭의 Command 입력란의 콤보 상자를 눌러 보면.. 달랑 revsvr32, Edit, Browse 이런 몇 가지 고정적인 아이템밖에 없다. 거기에다가 사용자가 이전에 등록한 적 있는 세트들이 같이 나오면 된다. 이 얼마나 깔끔한가?
EXE라면 Command가 바뀔 일은 별로 없겠지만 인자에 대한 세트 관리 기능이 있다면 충분히 유용할 수 있다.
IDE에 이런 기능이 없으니 날개셋 같은 개인 작품에서나 회사 제품 코드에서나.. 디버깅을 위해 사용할 다양한 프로그램들 경로를.. 소스 코드 주석이나 별도의 텍스트 파일에다 따로 메모해 놓는 촌극이 벌어지고 있다.

세트 데이터는 굳이 해당 프로젝트 파일에다가 저장하지 않아도 된다. 프로젝트/솔루션에 의존할 필요 없이, 그냥 그 프로그램 자체의 history data 명목으로 관리하는 형태로 제공되어도 충분히 편리할 것 같다.

2. 코드 자동 서식 적용

요즘 Visual C++ IDE에는.. 코딩을 하면서 닫는 중괄호나 세미콜론이 입력됐을 때, 각종 변수와 연산자· 토큰 사이에 공백을 균일하게 삽입하거나 없애고 탭 들여쓰기도 일관되게 맞춰 주는 '자동 서식' 기능이 제공된다. 쉽게 말해 whitespace에 대한 formatting 말이다. 이 옵션이 기본적으로 켜져 있다.

내 기억이 맞다면 이건 Visual C++ 2013쯤부터 처음으로 도입됐다. 2012에는 아직 확실하게 없었다.
베이직은 1980년대 도스 시절 QuickBasic에서부터 있었으며 C#도 최소한 200x 버전에서는 들어간 기능이지 싶은데 C++은 이제야 도입됐다.

다른 언어들은 문장을 완전히 파싱해서 내부 representation tree로 바꾼 뒤, 그걸 텍스트로 재구성함으로써 서식도 덤으로 적용되는 것이겠지만, C++은 그럴 수는 없지 싶다. 진짜 기계적이고 lexical한 문자열 치환 수준에서만 서식이 적용되지 싶다.

자동 서식 기능이 전반적으로는 괜찮은 편인데.. int *a, *b는 왜 int* a, * b라고 공백을 어색하게 배치하나 모르겠다. D처럼 int* a,b라고 썼을 때 b까지 포인터형이 되는 언어라면 모를까, 포인터형 별표와 변수명 사이에 공백이 들어가야 할 필요는 느껴지지 않는다.

그리고 배열 delete인 delete[]도 토큰 배치가 약간 기괴하긴 하지만.. 개인적으로는 붙여서 delete[] ptr; 이러는 걸 선호한다. 거기까지는 괜찮은데 delete []a를 다 붙여서 delete[]a로 바꾸는 건 좀 의아하다. 차라리 delete[] a라고 해 주지..
비슷한 맥락으로로, 함수의 인자로 배열의 포인터를 전달하는데 TYPE(*arg)[4] 같은 것을 한데 다 붙여 버리니 이 또한 어색하고 이상하다.

이런 것들이 C++의 자동 서식은 완전한 파싱을 거쳐서 적용되는 게 아니기 때문에 발생하는 부작용이지 싶다. 그러니 매크로나 템플릿 내부 같은 데서도 정확한 동작을 기대하기 어렵다.

3. 2019, 대화상자 리소스 에디터 뻗음

Visual Studio IDE는 2012~2013 즈음부터 외형이 크게 바뀌지 않기 시작했기 때문에 특히 2015와 2017은 내 경험상 거의 분간이 안 된다. 영문판은 웬일로 FILE EDIT 등 메뉴 이름을 잠깐 몽땅 대문자로 표기하는 객기(?)를 부리기 시작했다가 후대 버전에서 객기를 접은 듯하다.
2019는 프로그램의 제목 표시줄이 없어지고 화면 첫 줄에 곧바로 메뉴가 표시되기 시작했다. 현재 열려 있는 솔루션의 이름은 메뉴의 오른쪽에 표시된다. 윕 브라우저들도 그렇고 요즘은 제목 표시줄을 없애는 게 유행이기라도 한가 보다. 게다가 쟤들은 메뉴조차 없애 버리고 Alt키를 눌렀을 때만 메뉴가 표시되게 해 놨다.

그렇게 프로그램의 외형이 야금야금 바뀌는 것이야 좋다고 치는데.. 왜 예전에는 경험한 적이 없던 버그까지 야금야금 끼어 들어가나 모르겠다.
우선 아주 불규칙하지만 분명한 빈도로.. 텍스트 에디터의 폰트가 본인이 수동으로 변경하기 전의 원래 폰트로 되돌아간다. 정확한 재연 조건은 모르겠다. Visual Studio를 열어 놓은 채로 며칠 간격으로 절전 모드에 들어갔다가 복구하기를 반복하다 보면 되돌아가 버린다.

그리고 C++ win32 리소스 중에서 대화상자 편집기만 제대로 안 열리고 프로그램이 무한 루프에 빠지며(= CPU 소모하면서) 응답이 멎는 문제가 있다.
잘 알다시피 Visual Studio 2012부터는 msi 파일을 생성하는 배포 패키지 프로젝트가 짤려서 기본 제공되지 않는다. 별도의 extension을 설치해야만 다시 지원된다. 본인은 회사에서는 그렇게 했다.

그런데 그 extension을 설치한 뒤부터 win32 프로젝트에서 대화상자 편집기가 열리지 않고 IDE가 얼어붙어 버렸다. 그래서 대화상자 리소스를 편집하는 작업을 할 수가 없어졌다.
뒤늦게 그 extension을 disable시키거나 아예 제거해도.. 버전 16.2.3 최신 업데이트를 적용해도, 심지어 Visual Studio를 재설치(복구)해도 그 문제는 해결되지 않았다! 이 VS 2019는 대화상자 리소스를 영원히 편집할 수 없는 절름발이 상태가 된 것 같다.

검색을 해 보니 이 문제는 VS 2019 초창기 시절부터 종종 보고되곤 했던 것 같다. 하지만 release candidate 수준의 옛날 일이지 최신 업데이트에 이르기까지 문제가 발생하거나 해결됐다는 얘기는 딱히 발견하지 못했다.
이러니 Visual Studio는 최신 버전이 구버전의 용도를 완전히 흡수· 대체하지 못하고 구버전도 여전히 병행해서 사용돼야만 할 것 같다. 결국 회사에서도 2010을 따로 설치해야 했다.

4. 2010, 동작은 하지만 이상한 경고 메시지

그럼 구버전은 아무 이상이 없느냐 하면 불행히도 그것도 아니다.
Windows 10 초창기에는 안 그랬던 것 같은데.. 운영체제 업데이트를 몇 번 거치고 나니 VS 2010 devenv.exe는 정체를 알 수 없는 이상한 에러 메시지를 한번 내뱉은 뒤에 실행된다.

The file C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Vsa.tlb could not be loaded. An attempt to repair this condition failed because the file could not be found.
Please reinstall this program.


이미 알려진 문제이며 .NET Framework 3.5를 설치한 뒤에 Visual Studio도 복구(재설치)하면 이런 메시지가 없어질 거라고 하는데..
프로그램 사용을 못 할 정도의 치명적인 오류는 아니니 귀찮아서 안 하고 지낸다. 어차피 VS 2010을 C# 같은 .NET 플랫폼 개발용으로 사용하는 건 아니니 말이다.

5. 컴파일러의 버그

하루는 32비트 정수와 16비트 정수를 인자로 받아서 이걸 한데 뭉친 64비트 정수를 되돌리는 정말 간단한 인라인 함수를 구현한 적이 있었다. 이렇게 생성된 값을 저장하고 불러오게 했는데.. 문제가 발생했다. 불러온 결과가 이전에 저장했던 결과와 일치하지 않고 프로그램이 제대로 동작하질 않았다.

곳곳에다 변수값을 화면에다 찍어 봐도 내가 짠 코드에는 좀체 문제가 없는 것 같고.. 듣도 보도 못한 이상한 값은 전혀 예상치 못했던 곳에서 갑자기 생기고 있었다.
비유하자면 MAKELONG(16012, 76)의 계산 결과값이 저장할 때와 불러올 때가 서로 다르다는 게 믿어지시는가? high word 쪽의 값이 내가 지정한 값이 아니라 32766 같은 엉뚱한 값을 기준으로 계산되었다.

해당 함수를 #pragma를 줘서 최적화를 끄고, 인라이닝을 해제하는 등 별짓을 해도 계산값이 교정되지 않았다. 컴파일러가 구형인 것도 전혀 아니고, 갓 업데이트 받았던 따끈한 Visual C++ 2019 16.3.2였다.
신기한 것은.. { return X|(Y<<32); } 대신

{
    auto ret = X|(Y<<32);
    TRACE("%d %d\n", X,Y);
    return ret;
}

이렇게 함수 인자를 강제로 화면에다 찍게 하면 버그가 발생하지 않고 계산이 맞게 되었다는 것이다.
하지만 저렇게 하지 않고 함수를 아예 #define 매크로 형태로 고쳐도 문제가 동일하게 발생하니.. 이 정도면 변수를 참조하는 코드 자체가 단단히 잘못 생성되고 있는 것이나 마찬가지였다.

수 년 전엔 bit rotation을 구현한 암호화 알고리즘에서도 release와 debug의 동작이 다르고 최적화 적용 여부에 따라 동작이 달라지는 현상을 발견하긴 했는데.. 이 문제는 그것보다도 더 심각한 문제였다.
물론 비트 연산이라는 공통점은 있다. 컴파일러가 << >> | 같은 연산자를 다루는 데서 무리하게 최적화를 시도하는가 보다.

결국 이 버그는 memcpy라는 무식하기 짝이 없는 물건을 동원함으로써 겨우 회피할 수 있었다. 64비트 정수에다가 일단 32비트 값을 대입한 뒤, 4바이트 오프셋에다가 16비트 정수를 강제로 복사하게 했다. 컴파일러가 memcpy는 어째 제멋대로 최적화를 안 했는지 이렇게 하니 프로그램이 깔끔하게 돌아가기 시작했다. 비트 엔디언 독립성은 물론 포기했다.

memcpy는 예전에 align이 맞지 않는 임의의 단위로 메모리를 읽고 써야 할 때.. x86 계열에서는 아무 문제 없다가 ARM 같은 CPU에서 멀쩡한 프로그램이 뻗을 때도 유용하고 사용한 적이 있다.. CPU 특성이나 컴파일러의 특성을 가리지 않고 제일 무식하고 확실하게 메모리를 읽고 쓰는 게 보장돼야 할 때 최후의 보루 역할을 하는 듯하다.
그나저나 컴파일러의 버그임이 명백한 이 현상은 도대체 왜 발생하는지, 해결할 방법이 없나 궁금하다.

이상이다.
본인은 예나 지금이나 개인용 컴터에는 VS 2003, 2010, 2019를 나란히 설치해 놓고 지낸다. 즉, 최신 버전 말고도 2003과 2010은 고정 설치라는 뜻이다.

한때는 최신 API에 대한 설명 때문에 201x의 도움말을 하드에 설치해 놓았으나, 요즘은 마소에서 로컬 도움말은 2015 이후로 업데이트도 안 하고 거의 버린 자식 취급하길래..
그건 포기하고 그냥 옛날 200x 시절의 MSDN을 고전 Windows API 및 기본 C/C++ 레퍼런스용으로 사용한다. 이걸로 충당이 안 되는 최신 정보는 인터넷 조회로 해결하고 말이다.

Visual C++ 201x 버전들에서 본인의 기억에 남아 있는 인상적인 변화 사항은 다음과 같다.

  • 2012: 흰 스킨 도입. Windows XP 타겟 지원을 최초로 중단했다가 별도의 툴킷으로 따로 제공 시작. Syntax coloring이 더 세분화됨. 정적 분석 기능 도입. 예전 같은 서비스 팩 대신, 업데이트 n 형태로 수시로 업데이트 되기 시작
  • 2013: 약간 푸르스름하면서 흰 스킨 도입. 코드 자동 서식 적용 시작, 커뮤니티 에디션 도입.
  • 2015: C 런타임 라이브러리 구조가 개편됨
  • 2017: 설치/업데이트 체계가 전면 개편됨. 안드로이드 등 별별 환경 개발까지 다 지원하기 시작. 오프라인 도움말 앱을 사실상 지원 중단
  • 2019: 프로그램 제목 표시줄이 없어짐. 스플래시 화면이 더 간지나게 바뀜. 색깔이 채도가 약간 더 올라가고 산뜻해짐. 처음 실행했을 때나 기존 솔루션을 닫은 직후에 통상적인 시작 페이지 대신, "원하는 작업을 선택하세요" 대화상자가 표시됨.

Posted by 사무엘

2020/01/04 08:34 2020/01/04 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1701

1.
먼 옛날에 기술이란 게 지금보다 비싸고 희귀하던 시절에는 컴파일러 자체가 유료화 대상이었다. Windows 플랫폼 SDK에 같이 들어있는 무료 기본 컴파일러는 상업용으로 따로 판매되는 Visual C++의 컴파일러와 같은 빡센 최적화 기능이 없었다. 사용자가 작성한 코드대로 돌아가는 실행 파일은 만들어 주지만 최적의 성능을 발휘하는 형태로 만들지는 않았다는 것이다.

그러다가 Visual C++ 2005 Express를 기점으로 고성능 컴파일러도 무료화되고 기본 제공되기 시작했다.
요즘 컴파일러의 최적화 테크닉은 학교에서 배우는 것처럼 (1) 쓰이지 않는 변수나 코드 제거, (2) 값이 뻔한 수식은 한 번만 미리 계산해 놓고 loop 밖으로 옮기기, (3) 변수는 적절하게 레지스터 등재, (4) 함수는 적절하게 인라이닝, (5) 최대한 병렬화, (6) CPU 명령 하나로 처리 가능한 단위로 묶기, (7) switch 분기를 매번 번거로운 if문이 아니라 테이블 참고 방식으로 변경... 같은 미시적인 것만이 그림의 전부가 아니다.

그야말로 번역 단위(소스 코드) 간의 경계를 넘나들고 컴파일러와 링커의 역할까지 재정립하면서까지 저런 최적화 테크닉을 적용할 부위를 판단할 필요가 있다. 그래서 컴파일러에는 전역 최적화라는 옵션이 도입되고, 링커에는 link time code generation이라는 옵션이 추가되어 서로 연계한다. 뭐, 대단한 기능이긴 하지만 이 정도로 최적화를 쥐어짜서 성능이 더 나아진 것 대비 프로그램의 빌드 타임이 너무 길어지는 건 별로 마음에 안 든다.

그리고 세상에 C++ 컴파일러가 MS 것만 있는 것도 아니고, 경쟁사들의 제품도 갈수록 성능이 좋아지고 가격이 저렴해지고 있으니 optimizing compiler 정도는 충분히 대중적인 영역으로 내려가게 됐다.

2.
한편, Visual Studio Express 에디션은 기업에서 사용이나 상업용 소프트웨어 개발까지 아무 제약 없는 무료인 대신, 리소스 편집기와 MFC 라이브러리가 없다. 지금도 그런지는 모르겠지만 처음엔 심지어 64비트 빌드 기능도 없었다.
그 뒤 VC++ 2013부터는 MFC와 리소스 편집기가 다 있는 Community 에디션이란 게 나왔다. 얘는 기능 제약이 없는 대신 개인 개발자나 스타트업 중소기업 수준까지만(인원 얼마 이하, 연 매출 얼마 이하..) 무료이다.

서로 조건이 다른 무료이기 때문에 VC++ 2013, 2015, 2017까지는 한 버전에 대해서 express와 community 에디션이 모두 나왔다. 하지만 express는 장기적으로는 community로 흡수되고 없어질 것으로 보인다.

3.
이렇게 프로그램을 기본적으로 만들고 빌드하는 데 필요한 도구들은 슬슬 무료화 단계에 들어섰고, 그 다음으로는 그냥 빌드만 하는 게 아니라 제품의 품질을 높이는 데 도움을 주는 도구, 대규모 공동 작업과 테스트를 위해 필요한 전문 도구들이 아직 유료이다. 어떤 것은 Visual Studio의 엔터프라이즈 같은 제일 비싼 에디션에서만 제공된다.

Visual C++ 2008에서는 GUI 툴킷이 feature pack을 통해 무료로 풀렸더니 2012부터는 정적 분석 도구가 무료로 풀렸다. 이걸 처음 써 본 소감은 꽤 강렬했다.

사실, 본인도 그저 닥치는 대로 코딩과 디버깅만 하는 것 말고 소프트웨어공학적인 개발 프로세스라든가 기술 문서 잘 쓰는 요령, 변수와 함수의 이름 잘 짓는 요령, 전문적인 테스트 절차와 프로파일링 같은 것을 잘 알지 못한다. 더 발전하려면 그저 무료로 풀려 있는 도구들만 쓰는 게 아니라 그 너머에 있는 도구들도 뭔지 알고 필요성을 공감할 정도는 돼야 할 텐데..

그렇잖아도 날개셋 한글 입력기도 어떤 형태로든 버그가 없었던 적은 거의 없었다. 그리고 개인 프로젝트뿐만 아니라 회사 업무도 늘 깔끔하게 마무리 짓지는 못해서 버그가 있는 채로 제품을 내고 후회한 적이 적지 않았다.
만들어진 제품의 품질을 검증하는 절차 자체는 소프트웨어뿐만 아니라 건축· 건설을 포함해 공학의 어느 분야에서나 다 필요하고 존재할 것이다. 그 가운데에 무형의 지적 산물인 소프트웨어만이 갖는 특수성 때문에 이 바닥에서만 통용되는 방법론도 있겠지만 말이다.

아무튼, 프로그래밍 툴의 제작사들이 흙 파서 장사하는 건 아닐 테니.. 이런 어마어마한 컴파일러쯤은 딴 데서 개발 비용을 회수할 통로가 다 있으니까 무료로 풀어 놓는 게 가능할 것이다.

4.
(1) 이렇듯, Visual Studio를 포함해 개발툴들은 갈수록 기능이 좋아지고 기술들이 상향평준화되고 있다. 일례로, 2017인가 2019부터는 코드 편집기에 일반 컴파일러의 경고/에러뿐만 아니라 정적 분석 결과까지 초록색 밑줄로 띄워 주는 걸 보고 무척 감탄했다. (NULL 포인터 역참조가 일어날 수 있습니다 따위..)

(2) 2017/2019부터는 도움말과 API 레퍼런스는 몽땅 인터넷으로 처리하고 로컬 오프라인용 Help Viewer를 더 관리하지 않으려는지? 설치 화면에서는 기본 선택돼 있지도 않으며, 컨텐츠 역시 2015 내용 이후로 달라진 게 없어 보인다.

(3) 그리고 Spy++의 64비트 버전은 왜 도구 메뉴에서 숨겨 버린 걸까?
한 프로그램만으로 32비트와 64비트를 통합해서 잘 동작하는 것도 아니고, "이 창의 메시지를 들여다보려면 64비트 Spy++를 실행해 주십시오" 안내를 해 주는 것도 아닌데 왜 이런 조치를 취했는지 이해할 수 없다. 사실은 32와 64비트를 가리지 않고 아주 seamless하게 동작하는 Visual Studio의 디버거가 정말 예술적인 경지의 대단한 도구이긴 하다.

(4) 시대 유행과 별 관계 없는 부분은 계속해서 안 바뀌기도 하는 것 같다.
도구상자를 customize할 때 콤보 박스의 길이 조절을 200x 시절처럼 마우스 드래그로 할 수 없고.. 2010 이래로 그냥 픽셀수 입력으로 때운 걸로 굳히려는가 보다.

무려 7년 전 글에서 지적했던 2. 메뉴 편집기의 우클릭 버그, 3. 툴바 편집기의 화면 잔상 역시, Visual Studio 2019 현재까지도 전혀 고쳐진 게 없다.
그리고 예전엔 안 그랬던 것 같은데 2019는 Visual Studio 텍스트 에디터의 폰트를 딴 걸로 바꾸고 껐다가 다시 켜서 곧장 반영되는 걸 확인까지 했는데.. 켠 채로 절전 상태로 몇 번 갔다가 돌아오면 폰트가 컴터에 따라 아주 가끔은 돋움이나 Courier New 같은 기본 글꼴로 돌아오는 것 같다.

Posted by 사무엘

2019/05/10 08:36 2019/05/10 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1617

Windows의 Region

Windows API에는 region(영역)이라는 일종의 자료구조 라이브러리가 있다. 얘는 2차원 래스터(픽셀/비트맵) 그래픽 공간에서 각 픽셀별로 "영역에 포함되냐 안 되냐"라는 일종의 '집합'을 표현한다.
그리고 운영체제는 이 자료구조를 이용하여 각종 그래픽이 그려지는 영역을 정한다. 즉, region은 클리핑(clipping) 영역을 표현하는 데 쓰인다는 것이다.

도스 시절의 여느 그래픽 라이브러리에도 간단한 사각형 영역에만 그림이 그려지게 하는 초보적인 수준의 클리핑 기능은 있었다. 하지만 Windows의 region은 여러 사각형이 겹친 것, 임의의 다각형, 원 등 아무 모양이나 표현하고, 그 영역 안에만 그림이 그려지게 만들 수 있다.

그도 그럴 것이 Windows 같은 GUI 운영체제라면 창들의 Z-order 같은 걸 구현하는 과정에서 밥 먹고 맨날 하는 짓이 정교한 클리핑일 수밖에 없게 된다. 뒤쪽에 있는 창 내용은 앞쪽에 있는 창의 영역을 침범하지 않고 그려져야 하기 때문이다. 그러니 그런 기능을 사용자들에게도 쓰라고 제공해 주는 게 결코 이상한 일이 아니다.

region은 가장 먼저, (1) 직사각형, 타원, 모서리가 둥근 직사각형, 다각형(CreatePolygonRgn, CreatePolyPolygonRgn)처럼.. 속이 닫힌 도형을 그리는 다양한 API를 통해 생성할 수 있다. 당연히 그 도형의 모양이 영역의 모양이 된다.

다음으로, GDI가 제공하는 기능인 (2) path로부터 region을 생성할 수 있다(PathToRegion). path란 마치 윤곽선 글꼴 글립처럼 직선(MoveTo, LineTo)과 곡선(PolyBezirTo)을 임의로 조합하여 어떤 궤적이나 경계선을 기술하는 자료구조이다. region과 달리 벡터 기반이며, 별도의 자료구조로 존재하는 게 아니라 DC의 내부 상태에 종속인 형태로 보관된다는 차이가 있다.

path를 사용하면 경계선이 베지어 곡선인 region을 만들 수 있으며, TextOut 같은 글자 출력 함수를 path에다 넣으면 임의의 글자의 윤곽선도 따서 고스란히 region으로 만들 수 있다. 커다란 두 글자를 포개 놓은 뒤, 겹치는 영역만 다른 색깔로 칠하는 게 region으로는 가능하다. 그 비결은 바로...

사용자 삽입 이미지

(3) CombineRgn이라는 함수를 통해 region 간에 일종의 집합 연산을 할 수 있기 때문이다. 두 region의 교집합, 합집합, 차집합을 구함으로써 더 복잡한 형태의 region을 만들 수 있다.

위의 그림을 보아라. 운영체제나 하드웨어 차원에서 제공되는 layer이나 alpha channel 합성 같은 걸 쓴 게 아니다. 옅은 회색(B), 짙은 회색(S), 검정(겹침)이 차지하는 영역을 2차원적으로 완전히 따로 떼어내서 서로 완전히 다른 색깔과 무늬로 칠할 수 있다. 뚝 떨어진 영역도 당연히 같이 감안해서 말이다. 이런 게 평범한 글자 찍기 API로는 가능하지 않을 것이다.
다만, region은 anti-aliasing을 지원하지 않는 boolean 흑백 자료구조이다 보니, 글자 경계가 거친 것은 아쉬운 점이며 요즘 그래픽 기술의 트렌드와 맞지 않다.

그리고 끝으로.. region은 내부 자료구조를 어느 정도 노출해 주고 있기까지 하다. 그래서 그걸 직통으로 저장하고 불러오는 식으로 생성할 수도 있다. 데이터를 얻는 함수는 GetRegionData이고, (4) 그걸로부터 region을 다시 생성하는 함수는 ExtCreateRegion이다.

어떤 방식으로 region을 생성했건, 얘는 내부적으로 크게 세 부류로 나뉜다. region을 생성하거나 받아들이는 함수들이 그 region의 유형을 리턴값을 통해 알려주기도 한다.

  • 아무 영역도 없는 공집합인 NULLREGION
  • 직교좌표 직사각형 하나로만 구성된 SIMPLEREGION
  • 그 외의 다른 모든 모양을 표현하는 COMPLEXREGION.. 얘는 내부적으로 2개 이상의 사각형, 아니 scan line들로 구성된다.

자연에서 관찰되는 힘이라는 것들이 중력이나 원자력과 관계 있는 게 아니면 나머지는 출처가 몽땅 전자기력이듯이(폭발력, 마찰력, 탄성, 자석, 정전기, 표면장력, 생물 근육 등등등..),
그리고 사람이 생성(...)되는 방식이 흙을 빚어서 직통, 여자의 씨 같은 극소수 예외를 제외하면 나머지 수십~수백 억의 인간들은 몽땅 남자의 씨 기반이듯이.. 그런 것처럼 region도 일상생활에서 보는 단순하지 않은 물건들은 몽땅 complex라고 생각하면 되겠다.

region이 내부적으로 구현된 방식의 특성상(벡터/오브젝트 기반이 아닌 비트맵/픽셀 스캔라인 기반) 무한을 구현할 수 있지는 않으니, 집합 연산에서도 not 연산인 여집합이 지원되지는 않는 걸 볼 수 있다. 차라리 이미 있는 집합끼리 차집합이 대신 지원되고 말이다.

region을 식별하는 핸들 내지 포인터 자료형은 HRGN이다. 그런데 Create...처럼 HRGN을 리턴값으로 주는 함수 말고, Get...Rgn, CombineRgn 이런 이름이면서 HRGN을 인자로 받는 함수들은... 이미 있는 HRGN에다가 값만 바꿔서 넣어 준다. 그런 함수를 쓰려면 null region 하나라도 미리 미리 생성해서 전해 줘야 한다.

그런데 Windows API에는 많고 많은 region 생성 함수들 중에 null region만을 달랑 생성하는 함수는 의외로 없다. 좌표가 모두 0인 직사각형.. CreateRectRgn(0,0,0,0) 이게 그냥 텅 빈 region을 생성하는 역할을 한다. 좀 교묘한 점이다.

그럼 이 region는 어떤 용도로 쓰이며, 할 수 있는 일이 무엇일까? 본인이 생각하기에 다음과 같다.

1. 단독

그냥 저거 자체만으로 뭔가 2차원 공간 상의 기하/집합 알고리즘 구현체로.. 다른 GDI API와의 연계 없이, 심지어 명령 프롬프트용 프로그램에서도 쓰일 수 있다. 펜, 브러시, 글꼴, 비트맵 같은 타 오브젝트들이 DC와의 연계 없이는 거의 쓸모없는 것과 굉장히 대조적이다.
하지만 region이 그렇게 단독으로 쓰이는 경우는 그리 많지 않아 보인다.

2. 창의 invalid 영역 표현

어떤 창의 앞을 가리던 다른 창이 없어지고 내 창의 내용이 다시 그려지게 됐을 때.. 그 이름도 유명한 WM_PAINT 메시지가 날아온다.
BeginPaint와 함께 제공되는 DC는 창 전체가 아니라 정확하게 다시 그려져야 하는 영역에만 그림이 그려지도록 클리핑 처리가 돼 있는데, 이 영역이 말 그대로 region으로 표현되며 GetUpdateRgn 함수를 통해 얻어 올 수 있다.

WM_PAINT 때 이 영역에 대해서 PtInRegion이나 RectInRegion을 적절히 호출하면서 그림을 그리면, 무식하게 화면 전체를 그리는 것보다 프로그램 성능과 반응성을 향상시킬 수 있다.
물론 DC 차원에서 클리핑 처리가 되는 것만으로도 화면 전체를 그리는 것보다는 속도가 향상되지만, 애초에 그리기 요청을 안 하고 CPU 계산을 안 하는 게 더 낫기 때문이다.

3. 내부 클리핑

운영체제뿐만 아니라 사용자 역시 임의의 region을 생성해서 클리핑 용도로 쓸 수 있다. 비트맵이 사각형 모양이 아니라 원 모양으로만 뿌려진다거나, 특정 글자 모양으로만 뿌려지게 할 수 있다는 것이다.
그렇게 하려면 HRGN을 DC에다가 지정하면 된다. 이것은 SelectObject로 해도 되고 SelectClipRgn으로 해도 된다. 완전히 동일하다.
단지, 클리핑을 해제하는 것은 SelectClipRgn로만 가능하다. HRGN 값으로 NULL을 전해야 하기 때문이다. null region은 그림이 전혀 그려지지 않게 하는 효과를 낼 테니까..

HRGN은 기술적으로는 HPEN, HBRUSH, HBITMAP, HFONT와 마찬가지로 여러 GDI 오브젝트 중 하나로 취급된다. 상술한 바와 같이 DC에 SelectObject 될 수 있으며, 소멸 함수가 DeleteObject인 것까지도 동일하다.
하지만 얘는 다른 오브젝트들과 달리 select나 get 될 때 내부 메모리가 복사될 뿐, 핸들값 자체를 주고 받지는 않는다. 즉, HRGN은

HRGN oldRgn = (HRGN)SelectObject(dc, newRgn);
(.....)
SelectObject(dc, oldRgn);

이런 식으로 운용되지 않는다는 것이다. 옛날 핸들값을 보관하고 되돌리는 식의 절차가 필요하지 않다.
DC와 region은 서로 따로 논다. 이렇게 설정한 뒤에 원래 있던 HRGN 핸들은 곧장 삭제해 버려도 된다.

본인은 MFC의 CGdiObject처럼 GDI 객체 핸들만 한데 뭉뚱그린 템플릿 클래스를 만들어서 쓰고 있다. (소멸자에는 DeleteObject가 있고..)
그런데 다른 오브젝트들은 template<T> Handle(T v=NULL) 이런 식으로 NULL이 default 인자인 생성자를 만들어서 초기화할 수 있는 반면,
HRGN에 대해서는 인자가 없는 경우에 대해 specialize된 생성자를 따로 만들어서 이때는 null region을 생성해 놓게 했다. 그래야 이놈을 region을 얻어 오는 다른 함수에다가 인자로 줄 수 있기 때문이다.

4. 칠하고 그리는 공간

그리고 region 자체가 도형을 나타내니 그 모양대로 클리핑이 아니라 내부를 칠하는 용도로 응당 활용할 수 있다. 내부를 칠하는 FillRgn과, 내부의 경계선을 그려 주는 FrameRgn이라는 함수가 제공된다.
흥미로운 것은 경계선을 그릴 때도 pen 대신 brush가 쓰인다는 것이다. region은 path와 달리 벡터 드로잉이 아니기 때문이다. 경계선은 그냥 픽셀 차원에서 색깔이 변하는 곳을 얼추 감지해서 표시해 주는 것일 뿐이다.

5. 창 자체의 외형

끝으로, region은 윈도우의 모양을 지정하는 용도로도 쓰인다. 통상적인 사각형 모양이 아니라 리모콘 같은 다른 물건처럼 생긴 프로그램 창, 현란한 스플래시 윈도우, 내부에 구멍까지 있는 윈도우.. 전부 SetWindowRgn 함수의 산물이다.
한번 SetWindowRgn에다 전해 준 HRGN은 이제 운영체제가 관리하기 때문에 사용자가 DeleteObject 하지 말아야 한다고 문서에 거듭 명시되어 있다.

SetWindowRgn를 지정하는 순간부터 그 창은 운영체제가 non-client 영역과 테두리 경계에 기본 제공하는 각종 테마와 반투명 프레임, 둥그런 테두리, 그림자 효과들로부터 완전히 열외된다. 그 대신 고전 테마의 완전 무미건조한 테두리만이 그려진다. 일반적으로는 당연히 아래처럼 그려질 프로그램 창이 위처럼 그려지게 된다는 뜻이다.

사용자 삽입 이미지

그런 창은 어차피 non-client 영역이 전혀 없이 외형을 사용자가 완전히 customize하는 형태로 쓰일 테니 말이다. 제목 표시줄과 테두리의 외형은 보급품 그대로이면서 중앙에만 region을 지정해서 총알 구멍 같은 게 숭숭 뚫린 프로그램 창 같은 건 만들 수 없다는 뜻이다.

보통은.. 공용 컨트롤 6.0 매니페스트가 없는 프로그램의 경우, 버튼 같은 컨트롤들이 구닥다리 고전 스타일로 그려지고 non-client 영역은 운영체제가 자동으로 쌈빡하게 그려 준다. (그림에서 아래 오른쪽) 그런데 SetWindowRgn을 지정하면 반대로 컨트롤들은 정상적으로 그려지는데 non-client 영역이 고전 스타일로 돌아간다는 게 흥미롭다.

단, 지난 Windows 2000부터는 SetWindowRgn가 아닌 다른 방법으로도 사각형 모양이 아닌 윈도우를 표현할 수 있게 되었다. 바로 layered window이다. WS_EX_LAYERED 스타일을 준 윈도우에다가 SetLayeredWindowAttributes 함수를 호출하면 (1) 이 창에 대해서 투명도를 지정할 수 있고, (2) 특정 RGB 값을 color key로 지정해서 그 색깔은 투명으로 처리할 수 있다. 배경색을 칠하는 것만으로 그 부위가 투명해지니, 번거로운 region보다 훨씬 더 편리해지기까지 했다.

과거 MS Office 97~2000 시절의 흑역사 중에는 'Office 길잡이'라는 "취지만 좋았다" 급의 물건이 있었다. 애니메이션 캐릭터가 화면에 나타나서 돌아다니는데.. 첫 도입되었던 97은 캐릭터가 사각형 창 안에 갇힌 형태였던 반면, 2000부터는 배경 없이 창이 시시각각 애니메이션 캐릭터 모양으로 변했다.

사용자 삽입 이미지사용자 삽입 이미지

이게 Windows 2000에서는 하드웨어빨을 탄 layered window로 비교적 간편하게 구현됐던 반면.. 9x에서는 일일이 window region을 바꿔 가면서 동작했다. 그 밑의 창은 매번 WM_PAINT가 발생하면서 성능 페널티가 장난이 아니었을 것이다.

본인은 먼 옛날에 이런 프로그램도 구경한 적이 있었다. 실행하면 만화영화 그림체로 그려진 병아리 한 마리가 마치 저 Office 길잡이처럼 튀어나왔는데, 그냥 가만히 있는 게 아니라 각종 프로그램 창들 위를 돌아다녔다. 나름 중력도 구현했던지라, 마우스로 집어다가 공중에다 옮겨 놓으면 아래의 근처에 있는 창으로 떨어지기까지 했다. 내 기억이 맞다면 꽤 재미있는 프로그램이었는데.. 지금 인터넷으로 다시 검색하고 구할 길이 없다.

이 프로그램에 대해서 본인이 현재 기억하고 있는 건 아마 일본에서 만들어진 걸로 추정된다는 것, 그리고 무려 Windows 3.x용 16비트 프로그램이었다는 것이다. 그 열악한 Windows 3.x에서도 타이머를 걸어서 최소화 아이콘에다가 애니메이션을 구현하고, 창의 경계가 시시각각 곡선 윤곽으로 바뀌는 저런 액세서리 프로그램이 만들어지기도 했다.

이렇듯, SetWindowRgn을 이용해서 이런 재미있는 활용을 할 수 있는데.. 날개셋 한글 입력기에서 사각형이 아닌 모양의 창이 나타나는 곳은 마우스 휠을 눌렀을 때 나타나는 동그란 자동 스크롤 앵커가 유일하다.

사용자 삽입 이미지

에디트 컨트롤은 자동 스크롤 모드가 없고, MS 오피스 제품들은 동그란 테두리 없이 그냥 배경에다가 회색 화살표가 나타나는 듯하지만.. 마소에서 만든 웹 브라우저(IE, Edge)에서는 앵커 윈도우가 나타난다. 날개셋의 앵커 윈도우도 먼 옛날에 얘를 참고해서 만들어진 것이다. 맨 처음에는 region만 쓰다가 이내 layered window도 사용하도록 형태가 바뀌었다.

여담: 좌표계 관련 문제

아, region의 경계면과 관련해서 주의해야 할 점이 있다.
같은 좌표를 줬을 때, 직사각형은 pen으로 그려지는 테두리와 region의 영역이 픽셀 단위로 정확하게 일치한다. 다시 말해 같은 RECT rc에 대해서 CreateRectRgn + SetClipRgn을 한 뒤에Rectangle을 호출한 결과는 클리핑을 안 했을 때와도 동일하다.

하지만 타원(Ellipse vs CreateEllipticRgn)이나 폴리곤(Polygon vs CreatePolygonRgn) 같은 다른 도형에 대해서는 이것이 성립하지 않는다. region은 오른쪽과 아래 끝의 1픽셀이 미묘하게 잘린다.

//직사각형
CRgn rg; CRect rc(10, 10, 80, 80);
rg.CreateRectRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Rectangle(rc);
rc.OffsetRect(40, 40); dc.Rectangle(rc);

//원
CRgn rg; CRect rc(110, 10, 180, 80);
rg.CreateEllipticRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Ellipse(rc);
rc.OffsetRect(40, 40); dc.Ellipse(rc);

//폴리곤으로 표현한 직사각형
CRgn rg; POINT pt[4] = {
{10, 100}, {80, 100}, {80, 170}, {10, 170} };
rg.CreatePolygonRgn(pt, 4, ALTERNATE);
dc.SelectClipRgn(&rg); dc.Polygon(pt, 4);

이 코드를 실행한 결과는 다음과 같이 차이가 난다.

사용자 삽입 이미지

폴리곤으로 직사각형 좌표를 지정해 줘도, 아예 직사각형 전용 생성 함수를 줬을 때와 달리, region은 영역이 살짝 덜 생긴다. 그래서 그 region 안에서 동일 좌표로 도형을 직접 그려 보면 테두리의 오른쪽과 아래쪽이 잘린다.

이를 감안해서 원형 region을 생성할 때는 그리기 함수일 때보다 1픽셀 정도 더 크게 원을 그리면 잘리는 현상은 막을 수 있다. 하지만 그래도 그리기 함수와 region 함수는 경계 계산 결과가 서로 미묘하게 달라서 직사각형일 때처럼 깔끔하게 일치하는 모양이 나오지 않는다.
그러니 region 자체의 경계를 그려 주는 FrameRgn 함수를 대신 쓸 수밖에 없다. 허나, 얘가 그려 주는 테두리는 전문적인 원 그리기 함수에 비해 표면이 거칠며 별로 예쁘지 않다.

본인은 처음에 이런 특성을 몰라서 한동안 삽질을 했었다. 이럴 때도 region 대신 layered window는 순수하게 그리기 결과에 따라서 투명색을 자동으로 처리해 주니 더욱 유용하다.

Posted by 사무엘

2019/04/12 08:34 2019/04/12 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1607

1. 재부팅 이벤트

본인은 지금까지 Windows가 시스템 종료/재시작/로그아웃 될 때.. 메시지에 즉각 응답하면서 정상적으로 돌아가는 프로그램이라면 종료 처리도 당연히 정상적으로 되는 줄로 알고 있었다.
사용자가 [X] 버튼이나 Alt+F4를 눌러서 종료할 때와 완전히 동일하게 말이다. WM_CLOSE에 이어 WM_DESTROY, WM_NCDESTROY가 날아오고, WM_QUIT이 도달하고, message loop이 종료되고, MFC 프로그램으로 치면 ExitInstance와 CWinApp 소멸자가 호출되고 말이다.

왜냐하면 시스템이 종료될 때 발생하는 현상이 언뜻 보기에 정상적인 종료 과정과 동일했기 때문이다. 저장되지 않은 문서가 있는 프로그램에서는 문서를 저장할지 확인 질문이 뜨고, 운영체제는 그 프로그램 때문에 시스템 종료를 못 하고 있다고 통보를 한다. 좋게 WM_CLOSE 메시지를 보내는 걸로 반응을 안 하는 프로그램에 대해서만 운영체제가 TerminateProcess 같은 강제 종료 메커니즘을 동원할 것이다.

그런데 알고 보니 그게 아니었다.
정상적인 종료라면 ExitInstance 부분이 실행되어야 할 것이고 프로그램 설정들을 레지스트리에다 저장하는 부분도(본인이 구현한..) 실행돼야 할 텐데, 내 프로그램이 실행돼 있는 채로 시스템 종료를 하고 나니까 이전 설정이 저장되어 있지 않았다.

꼭 필요하다면 WM_ENDSESSION 메시지에서 사실상 WM_DESTROY 내지 ExitInstance와 다를 바 없는 cleanup 작업을 해야 할 듯했다. 뭐, 날개셋 한글 입력기에서 이 부분이 반영되어 수정돼야 할 건 딱히 없었지만 아무튼 이건 내 직관과는 다른 부분이었다.

시스템이 종료될 때 발생하는 일은 딱히 디버거를 붙여서 테스트 가능하지 않다. =_=;; 로그 파일만 기록하게 해야 하고, 매번 운영체제를 재시작해야 하니 가상 머신을 동원한다 해도 몹시 불편하다. 꽤 오래 전 일이 됐다만, 날개셋 외부 모듈이 특정 운영체제와 특정 상황에서 시스템 종료 중에 운영체제 시스템 프로세스 안에서 죽는 문제가 발생해서 디버깅을 한 적이 있었다. 몹시 골치 아팠었던 걸로 기억한다.

2. 파일의 동등성 비교

파일 이름을 나타내는 어떤 문자열이 주어졌을 때,

  • 상대 경로(현재의 current directory를 기준) vs 절대 경로
  • 과거 Windows 9x 시절이라면 ~1 같은 게 붙은 8.3 짧은 이름 vs 원래의 긴 이름
  • 대소문자 (Windows는 대소문자 구분이 없으므로. 그런데 이것도 A~Z 26자만 기계적으로 되나? 언어별로 다른 문자의 대소문자는?)
  • 그리고 옵션으로는 정규 DLL search path와 환경 변수

이런 것들을 몽땅 다~ 감안해서 다음과 비스무리한 일을 하는 가벼운 API가 좀 있으면 좋겠다. 보다시피 동일한 파일을 표현하는 방법이 굉장히 다양하기 때문이다.

  • 두 개의 파일 명칭이 서로 동일한 파일인지 여부를 판단. 당연히 파일을 직접 open/load하지 않고 말이다.
  • 그 파일의 대소문자 원형을 유지한 절대 경로. 일명 '정규화된' 이름을 되돌린다. 이게 동일한 파일은 물리적으로, 절대적으로 동일한 파일임이 물론 보장된다.
    참고로 Windows API 중에서 얼추 비슷한 일을 한다고 여겨지는 GetFullPathName이나 GetModuleFileName 함수는 절대 경로 말고 파일의 대소문자 원형 복원이 제대로 되지 않는다.
  • 혹은 그 파일의 시작 지점이 디스크에서 물리적으로 어디에 있는지를 나타내는 64비트 정수 식별자를 구한다. 포인터가 아니지만 파일의 동등성 여부 판단은 가능한 값이다. 정수를 넘어 UUID급이어도 무방함.

본인은 비슷한 목적을 수행하기 위해, 그냥 두 파일을 CreateFileMapping으로 열어 보고 리턴된 주소가 동일하면 동일한 파일로 간주하게 한 적이 있었다. 핸들 말고 MapViewOfFile 말이다. 본질적으로 동일한 파일이라면 운영체제가 알아서 같은 주소에다 매핑을 하고 레퍼런스 카운트만 증가시킬 테니까..
Windows 9x에서는 주소가 시스템 전체 차원에서 동일하겠지만 NT 계열에서는 한 프로세스 내부에서만 동일할 것이다.

하지만 내가 필요한 건 파일의 내용이 아니라 그냥 두 파일의 동등성뿐인데 이렇게 매핑을 하는 건 overkill 삽질 같다는 인상을 지울 수 없었다.
비슷한 예로, LoadLibrary도 같은 파일에 대해서는 같은 리턴값이 돌아온다. HMODULE도 오늘날은 핸들이 아니라 메모리 map된 주소이니까.. 다만, 오버헤드를 줄인답시고 LoadLibraryEx + LOAD_LIBRARY_AS_DATAFILE 이렇게 열어서는 안 된다. 그러면 로딩 방식이 크게 달라지더라.

3. JNI에서 문자열 처리하기

Java 언어는 JNI라고 해서 자기네 바이트코드 가상 머신이 아닌 C/C++ 네이티브 코드를 호출하는 통로를 제공한다.
프로그램 자체를 C/C++로 짜던 시절에는 극한의 성능을 짜내야 하는 부분에 어셈블리어를 집어넣는 게 관행이었는데.. 이제는 일반적인 코딩은 garbage collector까지 있는 상위 계층에서 수행하고, 극한의 성능을 짜내야 하는 부분에서만 C/C++ 코드를 호출한다는 게 흥미롭다.

JNI는 그냥 언어 스펙에 가까운 광범위한 물건이다. Windows 환경에서는 그냥 Visual C++로 빌드한 DLL이 export하는 함수를 그대로 연결할 수도 있다. 물론 그 DLL을 빌드하기 위해서는 Java SDK에서 제공하는 jni 인터페이스 헤더와 static 라이브러리를 사용해야 한다.
한편, 안드로이드 앱 개발에서 쓰이는 NDK는 JNI 스펙을 기반으로 자체적인 C++ 컴파일러까지 갖춘 네이티브 코드 빌드 도구이다.

Java의 문자열은 JNI에서는 jstring이라고 내부 구조를 알 수 없는 자료형의 포인터 형태로 전달된다. C++에서는 UTF-8과 UTF-16 중 편한 형태로 바꿔서 참조 가능하다.
UTF-8로 열람하려면 JNIEnv::GetStringUTFChars를 호출하면 된다. 길이를 알아 오려면 GetStringUTFLength부터 호출한다. 전해받은 문자열 포인터는 ReleaseStringUTFChars로 해제한다.

그 반면, UTF-16 형태로 열람하려면 위의 함수 명칭에서 UTF를 빼면 된다. GetStringChars, GetStringLength, ReleaseStringChars의 순이다. Java가 내부적으로 문자를 2바이트 단위로 처리하기 때문에 이들이 주로 취급하는 자료형은 jchar*이다. 그러니 얘는 char16_t 자료형과 호환된다고 간주해도 좋다. 참고로 wchar_t는 NDK 컴파일러의 경우 4바이트로 처리되더라.

UTF-16이나 UTF-8이나 다 UTF이긴 마찬가지인데, Java는 변별 요소인 8을 생략하고 함수 이름을 왜 저렇게 지었나 개인적으로 의구심이 든다. 물론 GetStringChars는 Java가 내부적으로 문자열을 원래부터 2바이트 단위로 처리하다 보니 우연히 UTF-16과 대응하게 됐을 뿐, 대놓고 UTF-16을 표방했던 건 아닐 것이다. 뭐, 이제 와서 그 체계를 바꾸는 건 불가능하고 "자바 문자열 = 2바이트 단위"는 완전히 고정되고 정착했지만 말이다.

또한 GetStringChars는 GetStringUTFChars와 달리 굉장히 치명적으로 불편한 단점이 하나 있다. 바로.. 변환된 문자열이 NULL-terminated라는 보장이 없다는 것이다!
그래서 본인은 이 포인터를 사용할 때 메모리를 n+1글자만치 또 할당해서 null문자를 추가해 주는 매우 번거로운 두벌일을 하고, 아예 클래스를 이렇게 따로 만들어야 했다. 좀 개선의 여지가 없으려나 모르겠다.

class CJstrToString16 {
    JNIEnv *_ev;
    jstring _jstr;
    const jchar *_ret;
    char16_t *_arr;
public:
    CJstrToString16(JNIEnv *ev, jstring js): _ev(ev), _jstr(js) {
        jsize n = ev->GetStringLength(js);
        _ret = ev->GetStringChars(js, NULL);
        _arr = new char16_t[n+1];
        memcpy(_arr, _ret, n*sizeof(char16_t));
        _arr[n] = 0; //고작 요거 하나 때문에..
    }
    ~CJstrToString16() {
        ev->ReleaseStringChars(_jstr, _ret);
        delete[] _arr;
    }
    operator const char16_t*() const { return _arr; }
};

4. Visual C++의 STL

C++은 타 프로그래밍 언어들과 달리, 심지어 전신인 C와도 달리, 언어가 개발되고 나서 자신의 특성을 잘 살린 라이브러리가 언어 차원에서 붙박이로 곧장 제정되지 않았던 모양이다. 그래서 각 컴파일러들이 중구난방으로 파편화된 형태로 라이브러리를 제공해 오다가.. 표준화라는 게 1990년대 말이 돼서야 논의되기 시작했다.

템플릿이 추가되어 C++에서도 제네릭, 메타프로그래밍이라는 게 가능해진 뒤부터 말이다. 처음에는 자료구조 컨테이너 위주로 STL이라는 이름이 붙었다가 나중에는 그냥 C++ library가 된 걸로 본인은 알고 있다.

Windows용으로 가장 대중적인 C++ 컴파일러야 두 말할 나위 없이 MS Visual C++이다. 얘는 거의 20여 년 전 6.0 시절부터 P.J. Plauger라는 사람이 구현한 C++ 라이브러리를 제공해 왔다. C 라이브러리와 달리 C++ 라이브러리는 소스가 비교도 안 될 정도로 복잡하고 난해하다는 것(암호 같은 템플릿 인자들..=_=), 그리고 저렇게 마소 직원이 아닌 개인 이름이 붙어 있다는 게 인상적이었다. 2000년대 초까지만 해도 휴렛-패커드라는 회사명도 주석에 기재돼 있었다.

P.J. Plauger는 현재는 Dinkumware라고 C++ 라이브러리만 전문적으로 관리하고 라이선스 하는 회사를 설립해 있다. 나이도 생각보다 지긋한 듯..
그런데 이런 세계적인 제품에 들어가는 라이브러리가.. 성능이 의외로 시원찮은가 보다. Visual C++이 제공하는 컨테이너 클래스가 유난히도 느리다고 까이는 걸 여러 사이트에서 봐 왔다.

최근에는 본인 직장의 상사마저도 같은 말씀을 하시기에 "헐~!" 했다. 업무상 필요해서 string, set, map 등을 써서 수십, 수백 MB에 달하는 문자열을 분석하는 프로그램을 돌렸는데, 자료 용량이 커질수록 속도가 급격히 느려져서 자료구조를 직접 새로 짜야 할 판이라고 한다.

난 개인적으로는 C++ 라이브러리를 거의 사용하지 않고, 더구나 그걸로 그 정도까지 대용량 작업도 해 보지 않아서 잘 모르겠다. 그 날고 기는 전문가가 만든 코드에 설마 그런 결함이 있으려나? 아니면 컴파일러의 최적화 문제인지?

글쎄.. 이런 게 있을 수는 있다. MFC의 CString은 그냥 포인터와 크기가 동일하며 값으로 전할 때의 reference counting도 처리한다. 그러나 std::string은 자주 쓰이는 짧은 문자열을 번거로운 heap 메모리 할당 없이 빠르게 취급하기 위한 배열까지 내부에 포함하고 있다. 이런 특성을 모르고 std::string도 함수에다 매번 value로 전달하면 성능에 악영향을 줄 수밖에 없다.

그런 식으로 임시 객체가 쓸데없이 생겼다가 사라지는 구조적인 비효율이 C++ 라이브러리에 좀 있는 걸로 들었다. R-value 참조자 &&가 도입된 것도 vector의 내부 처리에서 그런 삽질을 예방하는 근거를 언어 차원에서 마련하기 위해서라지 않는가? 그리고 Visual C++이 그런 비효율을 보정하는 성능이 좀 시원찮다거나 한 것 같다. 전부 다 그냥 추측일 뿐이다.

그러고 보니 cout<<"Hello world"가 printf("Hello world")보다 코드 오버헤드가 작아지는 날이 과연 올지 모르겠다. 이것도 그냥 떡밥인 건지..?? =_=;;

Posted by 사무엘

2019/04/09 08:32 2019/04/09 08:32
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1606

* 2014년에 썼던 글을 보완하여 다시 올린다.

옛날에 도스 시절에는 일명 '외부 명령'이라 하여 별도의 프로그램 형태로 존재하는 명령들이 있었다. format.com, diskcopy.exe 같은 것들.
이것들은 자기가 소속된 도스 버전을 가려서 동작했다. 가령, MS 도스 5.0이 설치된 컴퓨터에다 도스 6.x에 존재하는 새로운 유틸리티를 복사해 와서 실행하면, 실행에 필요한 파일들이 다 있다 하더라도 '도스 버전이 다릅니다'라는 에러 메시지와 함께 프로그램이 그냥 실행되지 않았다. 이것은 운영체제의 버전을 가려 가며 실행하는 프로그램을 본인이 난생 처음으로 접한 사례였다.

Windows에도 자신의 버전을 알려 주는 API가 응당 존재한다. 하지만 이건 지금 구동 중인 운영체제가 무엇인지를 알려 주는 편의 기능을 구현할 때나 사용할 만한 기능이다. 일반적인 프로그램이라면 About 대화상자 같은 데서 말이다.
만약 프로그램이 운영체제의 버전을 가려 가며 실행해야 한다면, 단순히 운영체제의 버전을 갖고 판단하는 건 썩 좋은 방법이 아니다. 내가 실제로 사용하고자 하는 기능을 요청해 보고(CoCreateInstance, LoadLibrary/GetProcAddress 등), 그 요청의 성공 여부에 따라 실행 여부를 결정하는 게 바람직하다.

뭐, 지금은 아무 의미가 없는 예가 돼 버렸다만,
가령 내 프로그램이 유니코드 API를 사용하기 때문에 Windows 9x에서는 실행을 거부해야 한다고 치자.
그렇다면 CreateWindowExW건 RegisterClassW건 유니코드 API를 실제로 호출해 본 뒤, 그게 실패하고 GetLastError()==ERROR_CALL_NOT_IMPLEMENTED가 돌아올 때 실행을 거부하면 된다. 운영체제의 외형보다는 그 운영체제의 실제 실행 결과를 보고 판단하는 게 낫다는 게 바로 이런 의미이다.

그런 것도 다 필요 없고 운영체제의 버전 숫자를 정말로 정확하게 알아 와야 한다면,
그 경우를 위해 태초에 GetVersion()이라는 간단한 함수가 있었다. 얘는 버전과 관련된 여러가지 정보들을 비트 자릿수별로 묶은 32비트 정수를 되돌렸다.

그 정보의 의미를 C언어의 비트필드 구조체로 나타내 보면 대충 다음과 같다. 주석으로 표시된 숫자는 윈도 7 기준으로 반환되는 값들이다.
(최신 Windows 10 기준의 반환값을 소개하지 않은 이유는 후술하도록 하겠다)

union WINVERSION {
    DWORD dwValue;
    struct {
        UINT nMajorVer: 8; //6
        UINT nMinorVer: 8; //1
        UINT nBuildNumber: 15; //7601
        UINT bWin9xOrWin32s: 1; //0
    };
};

WINVERSION os;
os.dwValue = ::GetVersion();

이 함수는 아무 매개변수도 필요하지 않으며, 리턴값도 DWORD 달랑 하나이니 미치도록 가볍고 사용하기 편하다. Windows 9x와 NT 계열이 공존하던 옛날에, 지금 운영체제가 (1) NT 계열인지를 알고 싶다면 GetVersion()&0x80000000 (최상위 비트)만 하면 OK였다.
그 뒤, NT 3.x인지 4.0인지, 9x 계열의 경우 95인지 98인지 ME인지 같은 건 (2) major와 minor 번호를 보고 판별하면 됐다. (3) 빌드 번호는... 딱히 막 중요한 정보는 아닌 듯하다.

그러나 이 함수는 문제점과 한계도 보였다. 한눈에 봐도 각 비트로부터 의미 있는 정보를 추출하는 게 매우 지저분하고 번거로웠다. HIWORD, LOBYTE 삽질이 싫다면, 저런 비트필드 구조체는 프로그래머가 재량껏 알아서 만들어야 했으며, 응용 프로그램이 이 정보를 잘못 취급하는 경우도 많았다.

비교할 필요가 없는 필드까지 다 비교를 해 버리는 바람에, Windows 95 이상에서 모두 동작할 수 있는 프로그램이 Windows 95에서“만” 동작하게 고정돼 버리기도 했다. 혹은 Windows NT 4.0이 NT 3.51보다 낮은 버전으로 취급되는 촌극도 벌어졌다. (리틀 엔디언 기준으로 저 구조체를 보면, minor 버전이 major 버전보다 더 높은 자릿수에 놓여 있음)

더구나 운영체제의 정체성을 나타내는 정보는 단순히 버전 번호와 빌드 번호 이상으로 더욱 복잡해져 왔다. NT 계열의 경우 당장 서비스 팩이 있고, 이게 무슨 에디션인지도(홈? 서버? 워크스테이션? 등) 알 필요가 있는데 단순히 숫자 하나만 달랑 되돌리는 함수로는 그런 걸 알려 줄 수가 없었다.

이런 문제를 해결하기 위해 Windows 95 내지 NT 3.5에서는 OSVERSIONINFO라는 구조체를 인자로 받는 GetVersionEx라는 함수가 추가되었다. major, minor 버전 번호와 빌드 번호, 운영체제 계열이 모두 독립된 구조체 멤버로 독립하였으며, (4) 서비스 팩 내역도 완전한 문자열 형태로 되돌려 주니 버전 정보를 다루기가 편해졌다.

이 구조체는 맨 앞에 자신의 크기를 써 주게 돼 있으며, 덕분에 추후 확장이 가능한 형태이다.
Windows 2000부터는 OSVERSIONINFOEX 구조체가 추가됐다. 확장된 구조체는 서비스 팩의 번호조차도 major와 minor 꼴로 받을 수 있으며, (5) 같은 NT 계열 중에서도 클라이언트 라인과 서버 라인을 구분할 수 있다(wProductType==VER_NT_WORKSTATION / VER_NT_SERVER). Windows XP와 Server 2003은 버전 번호가 5.1과 5.2로 서로 달랐지만, 후대 버전부터는 버전 번호는 동일하고 이걸로 구분을 해야 한다. (Vista / Server 2008, 10 / Server 2016 같은..)

그리고 클라이언트 라인은 XP 이래로 오늘날의 10까지 (6) home과 pro 에디션 구분이 거의 관행이 돼 있는데.. 이건 wSuiteMask 멤버의 비트 플래그 VER_SUITE_PERSONAL (0x200)의 존재 여부로 판별 가능하다. 저 플래그가 존재하는 게 home 에디션이다.
VER_SUITE_* 다른 플래그들 중에는 Windows XP의 embedded 에디션, enterprise 에디션 같은 걸 나타내는 것들도 있으니 참고하면 된다.

요컨대 9x/NT 이후로도 클라이언트/서버, home/pro 같은 복잡한 구분이 계속 이어지는 것을 알 수 있다. 그래도 GetVersionEx 한 방이면 모든 정보를 얻을 수 있다.

이걸로 모든 이야기가 끝이 났으면 좋겠지만.. 아이고, 끝이 아니다. GetVersionEx 함수는 2010년대 이후로 마소의 정책상 사용이 더 권장되지 않는 deprecate 판정을 받고, 시간이 정지해 버렸다.
이 함수는 아무런 단서가 없는 환경에서는 Windows 8, 즉 버전 6.2보다 더 높은 값을 되돌리지 않는 샌드박스가 되었다. 실제로는 이 컴퓨터에 Windows 8.1이나 10이 돌아가고 있더라도 말이다. 이와 관련된 더 자세한 정보를 원한다면 다음 URL을 참고하시기 바란다.

이제 이 함수는 응용 프로그램에게 그 응용 프로그램보다 나중에 출시된 운영체제에 대한 정보는 주지 않기로 작정한 듯하다. GetVersionEx가 샌드박스 없이 실제 자기 버전을 되돌리는 조건은 다음과 같다.

  • 응용 프로그램의 manifest XML에(compatibility-application-supportedOS) 그 운영체제의 GUID가 등록되어 있다.
  • 혹은 응용 프로그램의 PE 헤더에 OS의 최소 요구 버전이 최신 운영체제의 버전으로 맞춰져 있다. Windows 8.1의 경우 6.3, Windows 10이라면 10.0이 되겠다.

운영체제와 함께 제공되는 메모장 같은 기본 프로그램들은 후자의 조치를 취한 상태이다. 이렇게 빌드된 프로그램에서는 GetVersionEx가 해당 버전을 정확하게 되돌린다. 하지만 이런 프로그램은 이전 버전 운영체제에서는 아예 전혀 동작하지 않으므로, 3rd-party 응용 프로그램이라면 이런 방법을 쓰기 곤란하다. 그러니 매니페스트 등록을 해야 한다.

물론 마소에서 2015년의 Windows 10부터는 기존 버전 번호 자체를 10.0으로 동결시켜 버리고 더 바꾸지 않기로 작정했다. 그러니 버전 번호 변경으로 인해 GUID를 또 등록하는 식의 혼란은 앞으로 더 없을 것이다.

운영체제의 버전의 절대값을 되돌리는 GetVersionEx 대신 마소에서 사용을 권장하는 함수는... 지금 운영체제의 버전이 응용 프로그램이 제시하는 버전보다 상대적으로 높은지 안 높은지 여부만을 되돌리는 VerifyVersionInfo 함수이다. 그리고 이걸 기반으로 IsWindows10OrGreater 같은 helper 함수들도 만들어져 있다. (VersionHelpers.h)

하지만 이 함수들도 내부적으로 GetVersionEx의 결과값을 기반으로 비교를 하는 것이기 때문에 앞서 언급한 샌드박스의 제약을 받는 건 마찬가지이다.

샌드박스 없이 운영체제의 정확한 버전을 얻어 오는 함수는 크게 두 군데에 있다.
먼저, 의외로 네트워크 API이다. 그렇다고 소켓 API 같은 건 아니고, Windows에서 독자적으로 제공하는 함수 중에 내 로컬 컴퓨터를 포함하여 원격 컴퓨터에 설치된 운영체제의 버전을 얻어 오는 함수가 있다. 대략 다음과 같이 코드를 작성하면 된다.

#include <LM.h>
#pragma comment(lib, "netapi32")

WKSTA_INFO_100 *p;
::NetWkstaGetInfo(NULL, 100, (LPBYTE *)&p);
printf("%d, %d\n", p->wki100_ver_major, p->wki100_ver_minor); //10, 0
::NetApiBufferFree(p);

저기 100은 수효를 나타내는 게 아니며 각각의 숫자들이 별개의 의미를 지님에도 불구하고, 상수 명칭이 존재하지 않아서 그냥 생으로 100을 넘겨 줘야 한다.
운영체제 버전 하나 좀 얻자고 웬 생뚱맞은 분야의 API를 써야 하는 것도 삽질스럽지만.. 저 함수를 통해서는 그냥 major와 minor 버전 번호만 얻을 수 있다. 서비스 팩이나 빌드 번호 같은 세부 정보는 얻을 수 없다.

저거 말고 다른 대안으로는.. ntdll.dll에 있는 native API인 RtlGetVersion을 써도 된다.
OSVERSIONINFO(EX)의 포인터를 받아들이고 정수값을 리턴하므로 prototype이 기존 GetVersionEx와 거의 동일하다.
단, native API 버전은 성공한 경우의 리턴값이 0이다. 리턴 타입이 BOOL이 아닌 셈이다.

얘는 Windows 8.1 내지 10 같은 요즘 운영체제에서는 잘 동작하는데, 과거의 Windows 2000에서는 GetVersionEx와 달리 서비스 팩 정보를 되돌리지 않았던 것으로 기억한다. 구형 OS에서는 오히려 기존 함수를 쓰는 게 더 낫다. 거 참..;;
Windows가 지난 20년 동안 운영체제의 버전과 제품 종류를 얻는 그 단순한 절차만 해도 얼마나 복잡하고 지저분해져 왔는지를 확인할 수 있다. 관련 여담을 몇 가지 더 남기는 것으로 글을 맺고자 한다.

  • OSVERSIONINFOEX는 C++ 상속 문법 같은 걸 이용해서 선언된 게 아닌 관계로, OSVERSIONINFO와는 언어 차원에서 아무런 연결 고리가 없다. GetVersionEx에다가 전달할 때는 OSVERSIONINFO*로 reinterpret_cast를 해 줘야 된다.
  • 과거 Windows XP에는 media center 에디션 내지 태블릿 PC 에디션 같은 바리에이션이 있었는데.. 이거 여부를 얻는 건 GetVersionEx가 아니라 GetSystemMetric라는 다소 생뚱맞은 함수에 있었다. SM_MEDIACENTER, SM_TABLETPC처럼 말이다 .
  • 끝으로, Windows 10부터는 (7) 릴리스 연-월을 나타내는 4자리 숫자가 사실상 버전 번호가 됐으니 이걸 표시해 줘야 할 것이다. 그런데 이건.. 본인이 아는 방법은 그냥 무식한 레지스트리 조회가 유일하며, 공식적인 API가 따로 있지 않다.;;;

Posted by 사무엘

2019/03/14 08:36 2019/03/14 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1596

1. 새 노트북과 새 폰

본인은 2010년대 초중반까지 노트북은 맥북을, 폰은 삼성 안드로이드 폰을 사용해 왔다. 그랬는데 노트북은 이제 성능이 너무 뒤쳐졌으며 배터리 용량도 너무 감소했다.

그리고 폰도 하필 약정이 끝날 무렵부터 배터리가 급격히 빨리 줄어들기 시작했으며, 그로부터 몇 달 되지 않아 걸핏하면 툭툭 꺼지기 시작했다. 폰이 꺼지지 않도록 잘 간수하는(?) 게 마치 옛날에 집안 불씨가 안 꺼지게 간수하는 것 같은 일이 됐다. 서비스센터를 방문해 보니 배터리나 전원 단자 문제가 아니라 순전히 메인보드의 문제라는 말을 들었다.

이런 이유로 인해 본인은 2019년 초의 비슷한 시기에 전화기와 노트북을 모두 교체하게 됐다. 폰은 고맙게도 남아도는 준중고 기기를 그냥 주신 분이 직장에 계셔서 본인은 신규 개통도, 번호 이동도 아닌 기기 변경이란 걸 했다. 그리고 졸지에 생각지도 않았던 아이폰 유저의 대열에 합류했다.
그 반면, 노트북은 맥OS를 굳이 더 쓸 필요를 느끼지 않아서 오랜만에 국산 일반 Windows 기계로 복귀했다. 예전과 반대로 전화기를 애플 것으로 쓰게 된 셈이다.

내가 원하는 노트북 컴터는..
굳이 그렇게 무리하게 얇거나 가볍지는 않아도 된다. 화면 해상도도 그냥 Full HD 1920*1080 급이면 넉넉하고 충분하다. 굳이 2000 안 넘어가도 된다.

또한, 요즘은 무선 인터넷 인프라가 워낙 널리 깔렸으니 광학 드라이브나 유선 랜 단자쯤은 본체에 안 달려도 크게 불편하지는 않은 지경이 된 거 같다.

다만, 램은 8기가가 뭐야 8기가.. 장난하나? 안 그래도 추후 확장하기도 어려운 주제에..
뭐, 저걸로도 Windows 10 깔고 작업 자체는 가능하다. 하지만 개발툴, 가상 머신, 5개 이상 탭을 열어 놓은 크롬 브라우저, 문서 작업을 위한 오피스 등등을 한꺼번에 너끈히 구동하면서 5년 정도 버티려면 요즘은 정말 못 해도 12기가 이상은 달아야 된다.

(메모리가 부족해서 프로그램을 그렇게 많이 한꺼번에 돌리지 못하면? 고성능 CPU라든가, 요즘 놋붉들이 그렇게도 강조하는 큼직한 화면이 별 의미가 없다. 그 넓은 화면에다가 여러 프로그램들을 도배하려 해도 메모리에서 결국 병목이 걸린다!)

Windows 7 이래로 10까지 Windows의 최소/권장 사양의 메모리 용량이 계속 똑같이 1GB/2GB(64비트 기준)인 건.. 개인적으로 좀 사기극이고 허위 과장 광고라고 생각한다. 전혀 현실적이지 않다.

그리고.. 본인은 아재 사고방식이어서 그런지, SSD를 영 미덥지 않게 생각해 왔다. 빠른 건 좋지만 용량이 너무 부족하기 때문이다. 하지만 세월이 흐르니 SSD의 가격도 놀라울 정도로 많이 떨어지고 512GB급 물건도 나오다니 기술의 발달에 경이로움을 느낀다.
기계식과 전자식의 차이는 확연히 드러난다. SSD가 아니면 이 정도 성능과 용량을 자랑하는 노트북 컴이 이렇게 조용하고 가볍고 빠르게 돌아갈 수가 없을 것이다.

2. 새 컴파일러

컴퓨터를 새로 세팅하면서 Visual C++도 드디어 최신 버전을 깔아 봤다. 전에도 얘기한 적이 있지만 IDE, 플랫폼 SDK, 그리고 컴파일러 툴킷이 서로 완전히 분리되고 제각기 따로 노는 게 가능해진 것은 매우 바람직한 변화이다. 그리고 IntelliSense DB를 저장하는 방식이 예전의 MS SQL Server Compact Edition (*.sdf)에서 SQLite (*.vc.db)로 바뀌어 있더라.

날개셋 한글 입력기의 소스를 빌드해 보니 다음 사항들만 '경고'로 걸리고 나머지는 문제 없이 컴파일 된다.

(1) GetVersionEx 함수를 사용하는 부분이 deprecated로 처리됐다. Windows 8.1 내지 10부터는 운영체제의 세부 버전을 체크하는 방식이 이 함수를 사용하지 않는 다른 쪽으로 바뀌었기 때문이다. 다만, 옛 함수가 strcpy 내지 gets처럼 보안 위험 때문에 deprecated 된 건 아니다.

(2) C++에서는 포인터를 정수형으로 typecast 하기 위해서 잘 알다시피 reinterpret_cast 연산자를 사용하는데, 포인터형보다 작은 크기인 char, short, UINT(64비트 환경 한정)로 바꾼다면 언제나 경고가 뜨게 됐다. 이건 static_cast로 형변환을 한 단계 더 거쳐야 된다.

(3) delay-load 콜백 함수의 포인터를 보관하는 전역변수가 보안 강화를 위해 const 형태로 바뀌었다. 이 변수는 이제 실행 도중에 값이 변경될 수 없으며, 우리 코드에서 이 변수를 정의하면서 단 한 번 주소를 지정해 줄 수만 있다.
const PfnDliHook __pfnDliNotifyHook2 = _DelayDLLNotifyHook; 이런 식으로 말이다.
이 변수는 실행 파일의 read-only 영역에 보관되기 때문에 const_cast 같은 연산자를 이용해서 값을 강제로 변경하려 하면.. 그냥 crash가 발생하게 된다.

1은 그냥 어쩔 수 없고, 2는 경고가 안 뜨도록 코드를 고치고, 3은 조치를 취했다.

3. 날개셋 한글 입력기의 개선 사항

이렇게 개인용 컴퓨터가 바뀌고 개발 환경이 바뀌자, 날개셋 한글 입력기에 고칠 점이 몇 가지 눈에 띄기 시작했다. 요런 것들을 반영하면 또 0.01 정도 버전 숫자를 올릴 수 있을 것 같다.

(1) 이제 Windows 10이 세상을 평정했으니.. About 대화상자에 표시되는 운영체제 버전 정보에서도 10일 때는 년/월 형태의 세부 버전 번호(1709, 1809 같은)가 나타나게 했다. 아래 그림의 위/아래가 before과 after이다.

사용자 삽입 이미지

(2) 입력 패드는 지금 떠 있는 도구들을 모두 닫으면 프로그램을 곧장 종료하게 하는 /S 옵션이란 게 있는데.. 이게 프로그램 내부 구조를 개편하는 과정에서 언제부턴가 제대로 동작하지 않게 된 것을 뒤늦게 발견했다. 다음 버전에서는 고쳐질 예정이다.

(3) 글꼴을 본뜰 때 CJK 확장 한자 B뿐만 아니라 그 뒤의 C, D도 추가로 본뜨게 했다. 지난번 emoji에 이어서 글꼴 본뜨기 스크립트가 또 바뀌었다.

이쪽 SIP(보충 한자 평면) 한자들은 글자 수가 너무 많은데 일상생활에서 별로 쓰이지 않는 것들이고, BMP(기본 다국어 평면)보다 지원 시기가 훨씬 더 늦은 점으로 인해.. 지원하는 글꼴들도 통상적인 BMP 영역 한자용 글꼴과는 달리 비트맵 글립이 들어있지 않은 편이다.
그래서 16픽셀 글꼴은 썩 보기 좋지 않으며, 24픽셀은 돼야 볼 만하다.

(4) Vista 이래로 줄곧 그랬는지 아니면 8/10에서 보안이 강화돼서 그런 건지는 모르겠지만.. UAC를 켜 놓은 상태에서 ProgramData 디렉터리에는 새로운 파일을 생성할 수는 있지만 프로그램이 설치해 놓은 파일을 사용자가 임의로 고치는 건 안 되는가 보다.

본인은 거기에 있는 파일은 사용자가 마음대로 수정할 수 있다고 가정하고 글꼴 본뜨기 스크립트 같은 일부 파일은 사용자가 곧장 열어서 사용할 수 있게 했었다. 그런데 그렇게 할 수 없다면... 데이터 파일을 운용하는 방식을 바꾸든가 해야 할 것 같다. 비슷한 문제를 날개셋 타자연습의 연습글 목록 xml에서 이미 한번 겪은 바 있다.

(5) 지금까지 사용자에게서 옛한글 관련 동작 때문에 예기치 않은 오동작 의심 증세 문의를 많이 받았다. 옛한글은 일상생활에서는 안 쓰이다시피하니.. 다음 버전에서는 프로그램을 기본 설치했을 때 '세벌식 옛한글'을 아예 빼 버릴까도 고민 중이다.
장기적으로는 한글 표현 옵션 탭에서도 한양 PUA 내지 유니코드 1.1 레거시는 더 꽁꽁 숨겨 놓고 접근하기 어렵게 할 계획이다. 이제 쓰일 일이 없어졌으니 말이다.

(6) 그리고 좀 더 유의미한 변화로.. 휠 내지 노트북 터치패드를 아주 미세하게 굴렸을 때의 스크롤 동작을 개선했다. 이에 대해서는 다음 항목에서 더 자세히 설명하도록 하겠다.

4. 세밀하게 움직이는 휠

마우스 휠이란 게 PC에 도입된 지 20년이 넘었다. 휠이 도는 단위는 기본적으로 칸 단위로 구획이 나뉘어 있으며, 한 칸 돌 때마다 WHEEL_DELTA (120)이라는 값이 전달된다. 120이라는 값은 약수가 많아서(2*2*2*3*5) 다양하게 쪼개기 편한 수라는 이유로 선택된 것이다.

마소에서는 지금은 마우스 휠이 칸이라는 덩어리 단위로 돌아가지만, 미래에 연속된 단위로 아주 부드럽고 세밀하게 돌아가는 휠도 등장할 수 있을 거라고 예상했다. 그래서 단순히 움직인 칸 수가 아니라 거기에 delta를 곱한 값을 되돌리게 여유를 두고 메시지를 설계했다.

저게 1990년대 중후반의 일이다. 하지만 본인은 부드럽게 세밀하게 돌아가는 마우스 휠은 아직까지 구경하지 못했다. 그 대신 그 동작은 노트북의 터치패드/트랙패드가 물려받았다. 처음에는 손가락의 궤적을 따라 마우스 포인터를 옮기는 기능밖에 없었지만 나중에는 패드의 오른쪽 끝을 수직으로 문지른다거나, 아무 곳이나 두 손가락으로 수직으로 문지르는 것을 스크롤로 따로 인식하게 된 것이다.

그 스크롤 요청을 하는 방식이 예전에는 그 윈도우에 대해서 SetScrollInfo/Pos 함수를 호출하고 WM_H/VSCROLL 메시지를 생성하는 것이었다. 화면 캡처 프로그램이 제공하는 스크롤 캡처 기능이 어떻게 구현됐겠는지를 생각하면 된다.

그랬는데 요즘은 WM_MOUSEWHEEL을 보내는 게 대세이다. 터치패드의 종류에 따라 동작이 케바케이긴 하지만, 이때 옛날 동작과의 호환을 유지하기 위해 WHEEL_DELTA의 배수 단위로 스크롤 요청을 하는 경우도 있고, 아니면 문지른 속도에 따라 그 이하의 값을 보내는 경우도 있다. 2010년대부터는.. 스크롤 하다가 손을 급격하게 확 떼면 스크롤도 마치 관성을 받은 듯이 한동안 지속되다가 멈추는 동작까지도 구현돼 있다.

스크롤 요청을 받은 윈도우가 그림이나 웹페이지처럼 원래 픽셀 수준의 부드러운 스크롤을 지원하고 있다면 WHEEL_DELTA로부터 산출한 비율만치 자연스럽게 스크롤을 하면 된다.
하지만 픽셀이 아닌 글자 줄 수 단위로 이산적이고 각진(?) 스크롤을 지원하는 윈도우는 사정이 다르다. 너무 작은 delta 값은 나눗셈 과정에서 아예 0으로 버려지고 없어져서 스크롤이 전혀 처리되지 않을 수 있다. 내부적으로 델타 값을 보관하고 있다가 그 합이 양수나 음수로 WHEEL_DELTA의 배수가 됐을 때 줄을 옮기도록 다소 번거로운 조치를 취해야 한다.

그런데 동일한 각속도로 굴렸어도 120 단위만 지원하는 휠을 굴렸을 때와, 그 이상 정밀한 휠을 굴렸을 때 들어오는 delta값의 규모가 서로 같지 않다. 휠의 종류를 얻을 수 있는 API는 내가 알기로 없다. 그렇기 때문에 그 종류는 WM_MOUSEWHEEL 메시지 때 날아오는 값들을 보며 얼추 짐작을 해야 한다. (120보다 작은 값이 들어오질 않는가?)

Windows의 리스트 박스에서 마우스 휠을 굴려 보면, 처음에는 목록이 한 줄 정도는 픽셀 단위로 답답하게 굼뜨면서 스크롤 되다가 그 뒤 나중에는 줄 단위로 비교적 빠르게 스크롤 되는 걸 볼 수 있다. 이게.. 휠의 종류와 동작 방식을 감지하는, 한번 "간보는" 동작이 아닌가 개인적으로 생각한다.

날개셋 한글 입력기에서 이런 정밀한 휠을 지원하도록 조치가 취해진 곳은 날개셋의 고유한 에디트 컨트롤, 문자표 리스트, 그리고 편집기의 화면 인쇄(300% 이상의 배율로 확대했을 때)창이다. 사실, 내 것이 아닌 어떤 노트북 PC에서 날개셋 편집기에서 휠 스크롤이 잘 안 된다는 걸 오래 전부터 어렴풋이 경험하긴 했는데 그 당시에는 그다지 문제 의식을 못 느끼고 넘어갔던 것 같다.

처음에는 휠을 굴렸을 때 키보드 포커스를 받고 있는 창이 스크롤 됐지만 Windows 8인가 10부터는 마우스 포인터가 놓여 있는 창이 스크롤 된다. 그리고 저렇게 정밀한 휠이 도입되고, 세로 스크롤뿐만 아니라 가로 휠 스크롤 메시지도 도입되니.. 휠도 마치 고해상도 dpi만큼이나 지금까지 은근히 많이 변했다.

Posted by 사무엘

2019/02/02 08:32 2019/02/02 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1582

오늘은 Windows라는 운영체제가 GUI 프로그래밍 용도로 제공하는 공용 컨트롤들 중의 하나인 리스트뷰(List-view) 컨트롤에 대해 자세히 알아보도록 하겠다. (이름에서 '뷰'는 종종 생략되기도 함)
결론부터 말하자면 얘는 정말 세심하게 설계된 다재다능한 요물이다. 동일한 규격을 가진 다수의 아이템들, 특히 그림과 글자가 같이 가미된 아이템을 표시하는 모든 방식과 가능성을 고려해서 만들어졌다. 그래서 정말 많은 기능들을 제공한다.

리스트뷰가 기존의 재래식 초간단 리스트박스와 다른 점은 다음과 같다.

  • 리스트뷰는 글자뿐만 아니라 곁들여진 그림도 태생적으로 같이 처리 가능하다. 리스트박스에서는 그림과 글자를 같이 표시하기 위해서 얄짤없이 owner-draw로 가야 했다.
  • 마우스의 동작이 다르다. 리스트박스는 내부를 왼쪽 버튼으로 아이템을 선택해서 드래그 하면 선택막대가 자동으로 쭉 바뀌며 스크롤도 된다. 하지만 리스트뷰는 그렇지 않다.
  • 키보드의 동작도 다르다. 아이템을 복수 선택할 때 리스트뷰는 Ctrl+화살표를 눌러서 포커스만 이동시키고 Ctrl+Space로 선택을 하지만 리스트박스는 Shift+F8과 space 같은 다른 글쇠를 사용한다. 리스트뷰는 F2를 눌러서 아이템의 이름을 바꾸는 기능도 있지만 리스트박스는 그렇지 않다.

아울러, 리스트뷰가 같이 추가된 공용 컨트롤인 트리뷰(Tree-view) 컨트롤과 다른 점은 다음과 같다.

  • 트리는 아이템 하나를 HTREEITEM이라는 별도의 자료형으로 식별하지만, 리스트는 그냥 인덱스 번호이다. 트리는 노드 포인터 기반의 이산적인 컨테이너를 쓰지만, 리스트는 내부적으로 배열과 유사한 컨테이너를 쓰는 듯하다.
  • 리스트는 아이템의 복수 선택이 가능하지만 트리는 그렇지 않다.
  • 트리는 리스트와 같은 다양한 view 모드가 존재하지 않는다.
  • 아이템의 텍스트를 진하게 표시하는 state 플래그가 트리에는 있지만 리스트에는 없다.

리스트박스와 위상이 비슷한 자매 컨트롤(?)은 콤보박스이다. 하지만 리스트뷰와 위상이 비슷한 자매 컨트롤은 트리뷰라고 할 수 있다.
왼쪽에 트리뷰, 오른쪽에 리스트뷰를 배치한 프로그램으로는 탐색기, 레지스트리 편집기, 시스템 정보 등 의외로 꽤 많다. 왼쪽에서 카테고리를 선택하면 오른쪽에서 세부 정보가 표시되는 것이다. 오죽했으면 Visual C++의 MFC 프로젝트 마법사에도 요런 형태의 프로그램을 만드는 템플릿이 제공될 정도이다.

옛날에는 리스트박스를 서브클래싱 해서 drag & drop을 구현하고, owner-draw와 item data를 이용해서 얼추 트리 계층 구조라든가 check list를 구현하고, 파일이나 디렉터리나 드라이브 목록을 채워 주는 리스트를 만드는 등.. 별별 짓을 다 했다. 그리고 Visual Basic 부류의 RAD 툴들은 그걸 미리 구현해 놓은 리스트를 컴포넌트 형태로 제공했었다. 하지만 리스트뷰와 트리뷰 공용 컨트롤이 등장하면서 리스트박스의 역할이 상당수 분담되었다.

Windows 탐색기의 보기 메뉴에서 보는 바와 같이 리스트뷰 컨트롤에는 다양한 보기 모드가 있다.

(1) 큰 아이콘
아이콘이 중심이고 이를 설명하는 주 텍스트가 아이콘의 하단 중앙에 찍힌다. 이걸로 끝. 아이콘의 크기는 무엇이 되어도 상관없지만 보통은 표준 아이콘 크기인 32*32 또는 그보다 약간 더 큰 48*48이 쓰인다.
탐색기에서 확대 배율 조정이 되는 대부분의 모드들은 이 모드에 속한다. 아이콘의 크기만 바꾸는 거니까.. (보통 아이콘, 큰 아이콘, 아주 큰 아이콘..) 또한 당장 바탕 화면에 표시된 아이콘들도 다 리스트뷰의 이 모드인 것을 알 수 있다.

사용자 삽입 이미지

(2) 작은 아이콘
글자의 크기와 대등한 크기인 작은 아이콘이 쓰이며, 아이콘의 아래가 아니라 오른쪽에 주 텍스트가 나란히 찍힌다.

사용자 삽입 이미지

(3) 목록
아이템 하나가 표시된 모습이 작은 아이콘 모드와 완전히 동일하다. 그렇기 때문에 '작은 아이콘'과 차이가 무엇인지 언뜻 봐서는 구분하기 어렵다. 하지만 작은 아이콘(+ 큰 아이콘도 포함)에서는, 아이템을 드래그 해서 화면의 아무 위치로나 옮길 수가 있는 반면, 목록 모드는 그렇지 않다. i째 아이템은 현재의 스크롤 위치 기준으로 반드시 그에 상응하는 위치에 있어야 하며, 아무 위치로나 옮길 수 없다.

사용자 삽입 이미지

(4) 자세히(일명 report view)
한 줄에 아이템이 오로지 하나만 찍힌다. 작은 아이콘, 주 텍스트, 그 다음으로 n개의 부 텍스트가 마치 표처럼 일목요연하게 표시된다. 즉, 이 모드는 부 텍스트를 표 형태로 모두 볼 수 있는 유일한 모드이며, 상단에 헤더 컨트롤이 등장해서 쓰이는 유일한 모드이기도 하다.

사용자 삽입 이미지

사실, 헤더 컨트롤만 별도로 따로 생성할 수도 있다. 얘만으로도 각종 메시지 스펙이 공개돼 있는 별개의 공용 컨트롤이기 때문이다.
하지만 우리가 아주 특수한 사연이 있어서 리스트뷰 컨트롤 같은 거창한 물건을 직접 자체 구현이라도 하지 않는 한, 헤더만 끄집어내서 사용할 일은 별로 없을 것 같다.

지금까지 소개한 4종류의 모드를 정리하자면, 아이콘 모드들은 align을 어찌 하느냐에 따라서 상하와 좌우 스크롤바를 모두 볼 수 있고, '목록' 모드는 좌우 스크롤바만 볼 수 있다.
'자세히' 모드는 개수가 초과될 때는 상하 스크롤바이고, 아이템을 표시하는 폭이 초과됐을 때만 좌우 스크롤바를 볼 수 있다.

그리고 아이콘 모드는 기존 리스트박스에는 전혀 없던 새로운 기능이며, 기존 리스트박스와 가장 비슷한 모드는 '자세히' 내지 '목록' 모드라는 것을 알 수 있다. 이 두 모드에서는 아이콘은 필수가 아닌 그냥 선택, 옵션이다. 기존 리스트박스처럼 그림 없이 글자를 출력하는 용도로만 써도 된다.

이들에 비해 '작은 아이콘' 모드는 정체성이 불분명해서 사실 잘 쓰이지 않는다. 아이콘을 강조하고 싶으면 '큰 아이콘'으로 가면 되고, 좀 더 예쁘게 일목요연하게 아이템들을 출력하려면 '목록'(간단히) 또는 '자세히'로 가면 되기 때문이다. 저 그림에서도 보다시피 작은 아이콘은 폭이 들쭉날쭉이어서 보기에도 별로 좋지 않다.

그래서 Windows XP에서는 제5의 새로운 모드가 추가됐다. 바로..

(5) 타일
큰 아이콘을 사용하는데, 주 텍스트는 아이콘의 아래가 아닌 오른쪽에 출력된다.
아이콘이 좀 큰 편이니 주 텍스트의 아래에도 여유 공간이 생기는데, 거기에는 부 텍스트 중에서 사용자가 지정한 것을 덤으로 출력해 준다.

사용자 삽입 이미지

이것도 굉장히 참신한 발상인 것 같다. 타일의 폭은 사용자가 임의로 지정 가능하다.
align은 아이콘 모드처럼 left와 top을 모두 지정 가능하다. 다만, 아이템들의 위치까지 아이콘 모드처럼 임의 지정 가능한지는 잘 모르겠다.

원래 리스트뷰 컨트롤의 보기 모드는 4종류이다 보니.. 윈도우 스타일에서 0부터 3까지 딱 2개의 최하위 비트를 사용하여 지정하게 돼 있었다.
컨트롤을 생성하고 아이템들을 잔뜩 추가한 뒤에도 모드를 변경할 수 있었다. SetWindowLongPtr을 이용해서 스타일 값을 변경하면 컨트롤이 이를 인식해서 모드를 변경했다.

그런데 제5의 모드는 이런 식으로 지정할 수 없게 됐다. 리스트뷰 컨트롤은 기능이 워낙 너무 많아서 스타일, 확장 스타일, 거기에다 자신만의 고유한 전용 확장 스타일까지(LVM_SETEXTENDEDLISTVIEWSTYLE) 비트 플래그들이 꽉 찼기 때문이다.
결국은 LVM_SETVIEW라고 보기 모드를 지정하는 전용 메시지가 추가됐다. 새로운 보기 모드를 겨우 하나 추가하기 위해서였다.

네이버나 다음의 블로그들만 들어가 봐도 제목 목록만 표시, 본문까지 약간 포함해서 타일 형태로 표시.. 처럼 적어도 두세 종류의 보기 모드가 있는 걸 알 수 있다. 리스트뷰도 그런 식으로 그림과 글자의 표시 비율, 아이템당 전체 크기 같은 다양한 변수를 이런 식으로 제어할 수 있다고 생각하면 된다.
아이콘이 들어갈 자리에 사람 얼굴이 들어가면 무슨 인사기록표나 선거 후보 목록을 출력할 수 있을 것이고, 한자가 들어가면 옥편· 자전 내용을 이런 식으로 출력할 수 있을 것이다.

그럼 이제부터는 리스트뷰 컨트롤의 주요 개념이나 기능에 대해서 분야별로 간단히 소개한 뒤 글을 맺도록 하겠다.

1. image list

리스트와 트리 컨트롤은 아이템들 옆에 출력할 다양한 종류의 아이콘 그림들을 한데 관리하기 위해서 무슨 HICON을 몇백 개 내부적으로 관리..하는 건 아니고 image list라는 자료 구조를 공통으로 사용한다. image list는 마치 애니메이션 프레임처럼 크기가 동일한 여러 그림들의 배열이라고 생각하면 되며, 아이콘 핸들도 물론 손쉽게 등록할 수 있다. 투명색은 이미지 내부의 특정 배경색 또는 별도의 마스크 비트맵 중 편한 것으로 지정 가능하다.

또한 트리에서는 작은 아이콘이라는 한 종류만 사용하지만, 리스트 컨트롤에서는 구조적으로 큰 아이콘, 작은 아이콘 두 종류를 나눠서 지정 가능하다.
그리고 한 아이템의 아이콘에 대해서 여러 종류의 이미지를 한데 겹쳐서(overlay) 지정할 수도 있다. 파일이라면 '바로가기'임을 나타내는 자그마한 화살표라든가, 버전 관리 시스템에서 Up-to-date, modified 같은 상태를 나타내는 자그마한 modifier 그림이 바로 아이콘 overlay를 이용해서 표시된 것이다.

2. 그룹 분류

Windows XP에서는 타일 모드에 이어 리스트뷰 컨트롤에 아주 획기적인 기능이 하나 추가됐는데, 바로 '그룹' 기능이다. 필요하다면 그룹 내지 카테고리라는 것을 등록해 놓은 뒤, 아이템들별로 소속 그룹을 지정하면 이것들이 그룹별로 분류되어 딱 일목요연하게 표시된다.

사용자 삽입 이미지

그룹이 처음으로 도입된 XP에서는 이것 말고 다른 기능은 없다. Vista에서는 그룹에 대해 [+], [-] 버튼을 눌러서 마치 트리 컨트롤처럼 collapse/expand이 되게 하는 기능이 추가되었다. 단, 응용 프로그램에서 그게 가능하도록 별도의 비트 플래그를 넣은 그룹에 대해서만 그렇게 동작한다.
그룹은 다른 보기 모드에서는 다 지원되고 '목록' 모드만 열외이다.

3. 수많은 기능과 복잡한 API

리스트뷰 컨트롤은 당장 마소에서도 적극 사용하고 있다 보니, 자기 필요에 따라서 이것저것 수많은 기능들이 추가돼 왔다.
특히 IE4 시절에는 Active 데스크톱이니 뭐니 하면서 뭐든지 웹페이지처럼 보이게 하는 게 유행이었다. 리스트뷰 컨트롤의 아이템을 클릭하는 것조차 밑줄 쳐진 링크를 클릭하는 것과 비슷하게 보이게 하는 옵션은.. 음~ 정말 비장함이 느껴진다.

리스트뷰는 기능이 너무 많고, 공용 컨트롤 특유의 그 조작감까지 더해져서 다루기가 귀찮고 까다롭다. 리스트박스처럼 간단하게 LB_ADDSTRING + "문자열" 한 방으로 아이템을 추가할 수 없다. 뭘 더하고 고치려면 기본적으로 LVITEM 구조체 선언하고 마스크 플래그 지정하고..

더구나 문자열 부분 멤버는 읽기 쓰기 겸용으로 모두 쓰인다. Set 용도로 읽기 전용 문자열 포인터를 집어넣으려 해도 부득이하게 PTSTR 멤버에다가 const_cast를 해 줘야 된다. PTSTR과 PCTSTR을 공용체로라도 좀 같이 넣어 주지 하는 아쉬운 생각이 든다.

그리고 아이템 drag & drop은 컨트롤에서 우리에게 이벤트만 날려 주고 그걸로 끝이다. 드래그용 이미지를 생성하고 마우스 포인터 모양을 바꾸고 실제로 drop 처리를 하는 것, 아이콘 모드의 경우 실제 위치를 변경하는 LVM_SETITEMPOSITION 요청 따위는... 머리부터 발끝까지 사용자가 전부 일일이 구현해야 한다. 이거 일이 여간 번거로운 게 아니다.

헤더 클릭 정렬도 마찬가지다. 컨트롤이 자동으로 해 주지 않는다. 클릭된 헤더에 대해서 오름차순/내림차순/무정렬 상태를 나타내는 ▲▼ 모양을 표시하는 것, 다른 헤더에 있던 마크는 제거하는 것까지 전부 미주알고주알 우리가 해 줘야 되며, 아이템 비교 함수도 우리가 공급해 줘야 한다.
좋게 말하면 customize의 폭이 큰 것이고 나쁘게 말하면.. 귀찮다. 물론 재래식 리스트박스는 한번 등록된 아이템은 텍스트를 고치거나 순서를 변경하는 기능 자체가 전무했으니 그것보다는 상황이 나아진 셈이다.

4. 시스템 색상 변경

어떤 윈도우가 WM_PAINT를 받아서 자기 내용을 그릴 때, 매번 GetSysColor나 GetSysColorBrush를 호출하고, 매번 색깔을 새로 지정하고 펜과 브러시를 새로 생성한다면.. 시스템 색상이 나중에 달라지더라도 별 상관 없다.
하지만 성능을 위해서 이런 GDI 개체를 보관해 놓는다거나, 특정 시스템 색상이 합성된 상태로 비트맵 같은 걸 저장하고 있다면(일종의 캐싱).. 그것들은 시스템 색상이 바뀌었을 때 갱신되어야 한다.

이 상태를 알리는 메시지가 바로 WM_SYSCOLORCHANGE이다. 이제는 macOS조차도 최신 10.14 '모하비'에서 Dark 테마가 추가되었으니 시스템 색상 변경과 비슷한 개념이 도입된 셈이다. Windows는 다른 색깔 테마들은 다 없어졌지만 고대비 블랙/화이트만이 특수한 용도로 남아 있다.

WM_SYSCOLORCHANGE는 top-level 윈도우들에게 전파된다. 차일드에 속하는 리스트뷰 컨트롤이 이 메시지를 직접 받지는 못한다. 아이콘을 사용하지 않을 때는 별 문제가 없는데, 아이콘을 사용하는 컨트롤에 대해서 이 메시지를 수동으로 전해 줘야 한다. 그리하지 않으면 화면 배경의 흑백이 바뀌어도 쟤는 그게 반영되지 않아서 색깔 배색이 어색해지더라.

색깔 변경 통지도 마치 클립보드의 내용 변경 통지처럼 원하는 윈도우가 신청하면 top/bottom 위상을 불문하고 직통으로 받을 수 있어야 하지 않나 싶다. 이렇게 부모 윈도우가 일일이 전해 줘야 하는 건 디자인상 문제가 있어 보인다.

5. Ctrl+휠 인식

리스트뷰 컨트롤 내부에서 마우스 휠이 굴러갔다면 그렇다면 창 내부를 스크롤 하면 된다. 즉, 자체적으로 처리하면 되고 굳이 부모 윈도우에게 알려 줄 필요가 없다.
하지만 Ctrl+휠은 화면 확대 배율을 변경하는 용도로 쓰이는 게 요즘 추세이다. 응용 프로그램마다 자기가 사용하는 리스트뷰에서 지원하고자 하는 모드가 다를 테니, 이를 운영체제에서 임의로 일괄적으로 자동 지원할 수는 없다.

결국 Ctrl+휠은 그냥 휠과는 달리 부모 윈도우로 통지해 주는 게 바람직해 보인다. 하지만 이와 관련된 event notification은 공식적으로 존재하지 않는다. 탐색기는 Ctrl+휠을 어떻게 구현했는지가 궁금해진다. 하긴, 탐색기는 리스트뷰 컨트롤도 워낙 많이 마개조했으니 윈도우 프로시저를 서브클래싱 해서 메시지 전체를 통째로 가로채 버렸다면 그 정도 구현쯤은 일도 아니긴 했을 것이다.

6. 체크리스트 모드에서의 버그

리스트뷰 컨트롤(그리고 트리 컨트롤도)에는 모든 항목들에 대해 체크박스를 넣는 옵션이 있다.
보통은 아이콘 자리에다가 체크박스 이미지를 집어넣는 꼼수를 동원해서 야메로 체크리스트 모드를 구현하는 편인데.. 이 옵션은 이미지와 별개로 체크박스를 또 표시해 준다.

이 기능은 공용 컨트롤이 처음 개발되던 때부터 있었던 건 아니고 Windows 98 + IE4 내지 5 타이밍 때 추가되었다. 이 기능이 처음부터 지원됐다면 리스트뷰 컨트롤의 Selection model이라는 속성 하에서 "단일 선택 / 체크박스 / 복수 선택" 중의 한 옵션으로 지원되는 게 바람직했을 것이다. 체크박스 모드에서 또 복수 선택을 사용할 일은 없을 테니까 말이다.

그리고 체크리스트 모드는 그 정의상 보기 모드들 중에서는 '목록' 모드와 가장 잘 어울리고 아니면 기껏해야 '자세히' 모드와도 추가로 어울린다. 큰 아이콘이 부각되는 모드와는 아무래도 영 어울리지 않는데, 그래도 원한다면 그 모드에서도 체크리스트를 사용할 수는 있다.
다만, 이 모드에서 키보드나 마우스로 체크 표시를 반복하면 선택막대가 갈수록 진해지는데.. 이건 명백히 버그로 보인다. 고전 테마나 XP~7 같은 구버전에서는 이런 현상이 없었고 Windows 8~10에서만 저런다!

사용자 삽입 이미지


Posted by 사무엘

2019/01/13 08:36 2019/01/13 08:36
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1575

1. WM_CREATE의 리턴값/타입에 의문

Windows에서 C/C++로 GUI 프로그래밍을 할 때 WM_CREATE 메시지는 기본 필수 0순위로 접하게 되는 물건이다. 메시지 번호부터가 WM_NULL 다음으로 당당하게 1번이다.
얘가 오면 윈도우 프로시저는 lParam의 값으로 날아온 CREATESTRUCT 구조체 내용을 참조하면서 자신에 대해 초기화를 하고, 필요하다면 자기의 위치와 크기도 변경하고, 내 밑의 차일드 컨트롤들도 적절히 생성하면 된다.

그런데 이 메시지를 처리하고 나서 되돌리는 리턴값은 약간 이상한 형태이다. 성공하면 0, 실패하면 -1을 되돌리라고 명시되어 있다. 윈도우 프로시저가 실패값을 되돌리면 CreateWindow(Ex) 함수의 동작도 실패하여 창이 생성되지 않으며, NULL이 돌아온다.

즉, WM_CREATE의 리턴 형태는 BOOL이나 마찬가지이다. 그런데 왜, 어째서 직관적인 TRUE (1) / FALSE (0)가 아니라 이것보다 1 작은 값 형태로 정해진 걸까? (0 / -1)
이 때문에 MFC에서도 CWnd::OnCreate는 리턴 타입이 int로 설정되었다. 하지만 얘는 성공/실패만 따지기 때문에 원래는 int가 필요하지 않다. 내가 실험해 보니 굳이 0이 아니어도 -1을 제외한 다른 모든 값들은 성공이라고 간주되기는 하더라.

WM_CREATE는 대화상자 프로시저(DialogProc)처럼 평소에는 BOOL을 되돌리지만 몇몇 소수의 메시지에 대해서는 예외적으로 정보량이 더 많은 리턴값을 직접 되돌려야 하기 때문에 불가피하게 INT_PTR 형태로 설계된 것도 아니다. 더구나 WM_CREATE의 전신격인 WM_NCCREATE는 평범한 BOOL TRUE/FALSE 형태인 것도 의문을 더욱 증폭시킨다.

이와 관련해 혹시 숨겨진 사연이 있는지 레이먼드 챈 아저씨가 블로그에서 한 번쯤 다뤘을 법도 해 보이는데 내가 검색한 바로는 의외로 없다.
"CreateFileMapping은 실패값이 NULL인데 CreateFile은 실패값이 왜 혼자 INVALID_HANDLE_VALUE (-1)인가요?"와 거의 같은 맥락의 내력 의문점인데도 말이다.

파일 API의 경우, 먼 옛날에는(16비트 시절?) CreateFile이 지금 같은 형태의 핸들값이 아니라 파일 식별자 번호를 되돌렸으며, 0도 특수한 용도이지만 올바른 파일 식별자 값으로 예약돼 있었기 때문에 실패값을 -1로 따로 정한 거라고 설명이 돼 있다.

그렇다면 WM_CREATE도 처음에 설계하던 당시에는 굳이 BOOL로 국한되지 않고 0을 포함한 다양한 범위의 성공 리턴값을 되돌릴 수 있게 만들었는데.. 그럴 필요가 없어지면서 결국 지금 같은 형태로 굳어진 게 아닌가 싶다.

2. NC 버전과의 관계, 창의 소멸

Windows의 메시지 중에는 클라이언트 영역의 바깥 테두리를 그리거나(PAINT, ACTIVATE) 거기 크기를 정하거나(CALCSIZE) 그 영역의 마우스 동작을 감지하는(MOUSE*, ?BUTTON*) 용도로 WM_NC*로 시작하는 것들이 있다. 여기서 NC는 non-client를 의미한다.

그런데 WM_CREATE와 WM_DESTROY에도 WM_NC버전이 있다. 이때 NC는 딱히 외관상으로 클라이언트 바깥의 테두리나 제목 표시줄 같은 걸 가리키지는 않으며, 다른 방향으로 의미를 갖는다.
소멸 버전의 경우, WM_DESTROY는 아직 자기 밑에 자식 윈도우들이 멀쩡히 남아 있을 때 호출된다. 즉, 호출되는 순서가 top-to-bottom이다. 그러나 WM_NCDESTROY는 WM_DESTROY가 전달되었고 자식 윈도우들이 모두 소멸된 뒤에 자식에서 부모 순으로 bottom-to-top으로 호출된다.

즉, 어떤 윈도우가 윈도우 프로시저를 통해 가장 마지막으로 받는 메시지는 WM_DESTROY가 아니라 WM_NCDESTROY이다. WM_QUIT은 아예 스레드의 메시지 큐 차원에서 Get/PeekMessage를 통해 전달받을 뿐, 특정 윈도우의 프로시저로 오지는 않으니까...

어떤 윈도우 핸들과 C++ 객체가 연결되어 있는 경우, WM_NCDESTROY에서 그 객체를 delete 해 주면 된다. 그 전 단계인 WM_DESTROY에서 delete를 해 버리면 아직 소멸되지 않은 자기 자식 윈도우가 부모 윈도우의 C++ 객체 같은 걸 여전히 참조할 때 문제가 발생할 수 있다.

사용자가 Alt+F4를 누르거나 창의 [X] 버튼을 누르면 그 윈도우로 WM_CLOSE 메시지가 전달된다. 시스템 메뉴에서 닫기를 누른 것도(WM_SYSCOMMAND + SC_CLOSE)도 디폴트 처리는 WM_CLOSE 생성이다.
그리고 이 메시지에 대해 윈도우 프로시저가 다른 처리를 하지 않고 DefWindowProc으로 넘기면 그때서야 이 윈도우에 대해 DestroyWindow 함수가 호출되고 WM_DESTROY와 WM_NCDESTROY가 차례로 날아온다.

DestroyWindow는 호출하는 주체와 동일한 스레드가 생성한 윈도우들만 없앨 수 있다. 프로세스/스레드 소속이 다른 윈도우는 없앨 수 없으며, 그런 윈도우를 상대로는 WM_CLOSE를 보내서 창을 없애 달라는 간접적인 요청만 할 수 있다.

악성 코드 급의 프로그램이 아니라면 이 요청을 무작정 거부하는 끈질긴 윈도우는 없을 것이다. 하지만 일반적인 프로그램의 경우, "이름없음 문서를 저장하시겠습니까?"라고 질문을 해서 사용자가 취소를 누르면 자기가 종료되지 않게 하는 처리 정도는 한다. 작업 관리자의 '프로세스 종료' 기능은 응용 프로그램 창에다가 WM_CLOSE부터 먼저 보내 보고, 그래도 말을 안 들으면 TerminateProcess라는 극약 처방을 하는 식으로 동작한다.

그런데 WM_DESTROY 메시지를 받았는데 자기 자신에 대해서 DestroyWindow를 또 호출하는 이상한 프로그램도 있는가 보다. 창을 없애라는 요청은 WM_CLOSE이고, 이때는 그냥 DefWindowProc만 호출해도 알아서 소멸이 된다. WM_DESTROY는 요청이 아니라 이 창이 없어지는 건 이미 정해졌고 피할 수 없는 운명이라는 통지일 뿐인데.. 이때 DestroyWindow를 호출하면 같은 창에 대해서 WM_DESTROY가 이중으로, 재귀적으로 전달되는가 보더라.

소멸 중인 윈도우에 대해서 DestroyWindow 요청은 가볍게 무시만 해도 될 듯하지만 이미 이런 식으로 정해지고 정착해 버린 동작은 호환성 차원에서 함부로 고치지는 못한다고 한다.

3. 창의 생성

소멸 얘기가 좀 길어졌는데...
생성 버전에 속하는 WM_CREATE와 WM_NCCREATE 짝은 소멸 관련 메시지와 같은 유의미한 차이가 없긴 하다.
자식 컨트롤들을 생성하는 건 그냥 WM_CREATE 때 하면 되지, NCCREATE 때 딱히 해야 할 일은 없다. 쟤는 그냥 NCDESTROY와 짝을 맞추기 위해 도입된 것에 가깝다.

어떤 창이 생성되면, CreateWindow(Ex) 함수가 실행되어 있는 동안 WM_CREATE만 오는 게 아니라 WM_GETMINMAXINFO, WM_NCCREATE, WM_NCCALCSIZE가 먼저 전달된다. CREATE말고 나머지 메시지들은 창 내부의 공간 배분과 관계 있는 것인데, 아주 특수한 형태로 동작하는 유별난 윈도우가 아니라면 그냥 다 디폴트로 넘겨도 무방한 것들이다.

그리고 앞서 살펴본 바와 같이, CREATE 계열 메시지들은 실패값을 리턴함으로써 이 창의 생성을 저지할 수 있다.
WM_NCCREATE의 실행이 실패한다면(FALSE) 이 창은 그 뒤로 곧장 WM_NCDESTROY만 날아온 뒤 소멸되어 버린다. 그러나 WM_CREATE에서 실패하면(-1) WM_DESTROY와 WM_NCDESTROY가 차례로 날아오면서 소멸된다.

그런데 여기서 유의할 것이 있다.
프로그램의 main 윈도우의 경우, WM_DESTROY를 받았을 때 대체로 main message loop을 벗어나고 프로그램 전체를 종료하기 위해서 PostQuitMessage를 호출한다.
이게 일단 호출된 뒤부터는 이 스레드에서는 다른 GUI 윈도우를 생성한다거나 message loop을 돌아서는 안 된다. 여기에는 에러 메시지를 출력하기 위한 간단한 MessageBox 호출도 포함된다.

main 윈도우의 생성이 WM_CREATE 단계에서 실패했다면(WM_NCCREATE은 무관) WM_DESTROY를 거치게 되며, 특별한 조치가 없는 이상 그 메시지의 handler에 있는 PostQuitMessage도 처리되었을 것이다. 이 상태에서

if(::CreateWindowEx( .... )==NULL) {
    ::MessageBox(L"프로그램 실행 실패");
    return 1;
}

이런 식으로 코드를 쓰면 MessageBox 내부의 메시지 loop은 메시지 큐에서 WM_QUIT이 튀어나오기 때문에 곧바로 끝난다. 즉, 메시지 박스가 화면에 표시되지 않는다는 것이다.
그러니 에러 메시지를 찍을 거면 차라리 WM_CREATE 내부에서 -1를 리턴하기 전에 하는 게 낫다.

심지어 main 윈도우의 WM_NCDESTROY에서 MessageBox를 호출하려 시도하는 경우도 있다고 한다. 프로그램 실행이 다 끝난 마당에 무엇을 찍을 일이 있는지는 모르겠지만 이 역시 위와 동일한 이유로 인해 메시지 박스가 화면에 나타나지 않는다.
뭐, WM_DESTROY 대신 WM_NCDESTROY에서 PostQuitMessage를 요청할 수도 있겠지만 int main(int argc, char *argv[]) 대신에 char **argv만큼이나.. 익숙한 관행은 아니어 보인다.

이상. 이렇게 간단하고 익숙한 주제를 갖고도 지금까지 진지하게 생각하지 못한 것에 대해 할 말이 많을 때가 흥미롭다.

Posted by 사무엘

2018/12/12 08:31 2018/12/12 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1564

« Previous : 1 : 2 : 3 : 4 : 5 : ... 11 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2020/07   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Site Stats

Total hits:
1410079
Today:
122
Yesterday:
617