« Previous : 1 : ... 15 : 16 : 17 : 18 : 19 : 20 : 21 : 22 : 23 : ... 32 : Next »

예전에 본인은 windowless 리치 에디트 컨트롤에 대해서 글을 쓴 적이 있었는데, 이번에는 눈에 보이는 컨트롤에 대해서 다루도록 하겠다. 난 일종의 텍스트 에디터 윈도우를 처음부터 끝까지 만들어 본 사람이다 보니, 이런 세부적인 디테일에 눈길과 관심이 간다.
여러분은 PC 사용자로서, 혹은 더 전문적인 프로그래머로서 Windows 운영체제의 에디트 컨트롤에 대해서 얼마나 알고 계신가? 그 이름도 유명한 메모장이 얘를 기반으로 동작하는 걸로 잘 알려져 있다.

얘는 서식이 없는 plain 소규모 텍스트에 대한 아주 기본적인 입력 기능만 제공하라고 만들어진 물건이다.
그래서 전문적인 에디터들이 내부적으로 줄 단위 연결 리스트 자료구조를 사용하는 것과 달리, 얘는 단일 배열 버퍼 기반이라는 게 가장 큰 특징 되겠다. 복잡한 메모리 관리 메커니즘이 전혀 없이 텍스트는 전체가 커다란 배열이며, 삽입이나 삭제는 진짜 직관적이고 단순하게 밀고 당기고 전체 메모리를 재할당하는 식으로 행해진다.

Windows 9x 시절까지만 해도 이 에디트 컨트롤은 64KB가 좀 덜 되는, 6만여 바이트 이상의 텍스트는 불러들일 수 없었다. 아무리 16비트 시절의 잔재라지만 이건 말도 안 되는 제약이었다.

오늘날의 운영체제에서야 그 정도의 막장 제약은 존재하지 않는다. 그래도 단일 버퍼 기반이며 근본적으로 대용량 텍스트를 다루는 데 최적화되지 않은 구조인 것은 변함없다. 그렇기 때문에 메모장에서 수 MB 이상의 파일을 불러들이고 편집하는 건, 불가능하지만 않을 뿐 여전히 무리다. 다른 프로그램을 써야 한다. 가령, 확장판인 리치 에디트 컨트롤은 연결 리스트 기반이며 에디트 컨트롤과 같은 제약이 없다.

간단한 물건답게 실행 취소(undo)는 직전의 동작 딱 하나만을 취소하거나 도로 철회하는 게 가능하다. 요즘 유행인 다단계는 지원하지 않는다.
텍스트 전체를 왼쪽뿐만 아니라 가운데나 오른쪽으로 정렬하여 출력하기, 숫자만 받아들이기, 알파벳은 대문자나 소문자 한 형태로만 받아들이기, 입력되는 텍스트를 '암호'로 처리하여 화면에 숨기기 같은 아기자기한 옵션이 있다. 최대 글자 수 제한을 걸 수도 있다.

얘의 동작 방식을 사소하게 바꿔서 배경이나 글자색을 바꾸고, 특정 문자는 안 받아들이게 하고, 문자열을 자동 완성한다거나 우편번호/시리얼 번호처럼 특정 형식을 만족하는 문자열만 입력되게 하는 것은... 아주 전형적인 Windows 프로그래밍 주제였다. 윈도우 프로시저를 서브클래싱하면 된다.
단일 버퍼라는 특성상 에디트 컨트롤의 메모리 핸들에 곧장 접근하여 조작하는 API도 있긴 하나, 실제로 사용되는 경우는 거의 없다. 사용이 권장되지도 않는다.

multi-line 모드의 경우 줄 바꿈 문자는 오로지 정확하게 \r\n만 지원하는 걸로 잘 알려져 있다. 그래서 유닉스 계열의 \n 텍스트를 불러와 보면 \n 문자가 실제로 보이고 줄 바꿈 처리가 되지 않아 텍스트를 제대로 볼 수 없다.
이 역시 리치 에디트 컨트롤과는 다른 점이다. 텍스트를 불러들이는 과정에서 딱히 줄 바꿈 처리를 전혀 하지 않고 normalize도 전혀 하지 않는 단순함을 추구했기 때문이랄까?

그리고 Windows GUI 프로그래밍을 좀 해 본 사람이라면 경험적으로 아는 중요한 특징이 하나 있다.
multi-line 모드에서 word-wrap(자동 줄바꿈) 옵션은 컨트롤이 생성될 때 한번 지정되고 난 뒤부터는 변경되지 않는다. 스타일을 바꿔도 바뀐 대로 동작하지 않는다.

그렇기 때문에 당장 메모장에도 있는 '자동 줄 바꿈' 옵션은 에디트 컨트롤을 파괴했다가 다시 생성하는 방식으로 구현되어 있다! 물론 창이 갖고 있던 내용을 따로 보관했다가 다시 가져와야 할 테고. 이건 Windows 95 이래로 지금까지 동작 방식이 시종일관 변함없다.

심지어는 닷넷이 제공하는 에디트 컨트롤의 관련 속성조차도 내부 구현은 창을 다시 생성하는 걸 Spy++ 같은 프로그램으로 확인할 수 있다고 한다. 핸들값이 바뀌니까 말이다.
응용 프로그램에다가 꼼수를 추가하면 추가했지, 에디트 컨트롤은 절대로 건드리지 않기로 작정하고 봉인을 시킨 모양이다. 그러니 앞으로 Windows 9, 10이 나오더라도 에디트 컨트롤이 연결 리스트 기반으로 바뀐다거나 하는 일은 없을 것으로 보인다.

한 에디트 창을 single line으로 쓰느냐 multi line으로 쓰느냐 같은 건 프로그램 실행 중에 동적으로 바뀔 일이 거의 없다. 그러니 그런 건 고정불변으로 둘 만도 하다.
하지만 multi line에서 자동 줄 바꿈 여부는 텍스트 에디터를 만드는 사람이라면 누구나 공감하듯이 동적으로 바뀔 여지가 있다. 그런데 그걸 불변으로 굳혔다는 것은, 에디트 컨트롤을 갖고 뭔가 진지하게 대용량 텍스트 에디터를 만드는 건 설계 차원에서 정말로 고려하지 않았다는 걸 의미한다.

뭐, 단순하긴 해도 운영체제가 원초적으로 제공하는 텍스트 입출력 기술의 혜택은 그대로 받는지라, 오늘날 에디트 컨트롤은 마냥 단순하기만 한 것도 아니다. Uniscribe를 사용하여 나름 아랍어 같은 complex script의 입력과 위치 계산도 기본적인 건 그럭저럭 처리해 낸다. 우클릭했을 때 나타나는 메뉴를 보면, 텍스트의 기본 진행 방향을 L2R로 할지 R2L로 할지 지정하는 옵션이 있다.

마지막으로 입력 쪽을 살펴보면, Windows에는 TSF라는 입력 기술이 도입됐는데, 에디트 컨트롤은 기본적으로는 얘를 완벽하게 지원하지 않는다. 문자 입력기가 모든 텍스트를 자유자재로 조작할 수 있는 TSF 프로토콜은 역시나 리치 에디트 컨트롤만이 Windows XP sp1 시절부터 도입했다. 비록 이것도 모든 리치 에디트에 자동 적용되는 건 아니고, 최신 버전의 컨트롤을 사용하고 전용 메시지를 사용해야 하지만 말이다. (리치 에디트는 일반 에디트와는 달리 업그레이드가 쭉쭉 되어 온 대신, 버전 내력이 꽤 꼬여 있다..)

그렇기 때문에 이 에디트 컨트롤을 기준으로는 TSF와 관련된 두 가지 접근이 있어 왔다.
첫째는, 에디트 컨트롤을 사용하는 응용 프로그램이 윈도우 프로시저를 서브클래싱하여, 적절한 시기에 TSF API를 호출하고 TSF 인터페이스를 직접 구현하는 것이다. TSF로부터 특정 문자열을 읽거나 쓰라는 요청을 받으면, 그걸 EM_SETSEL, EM_GETSEL 메시지 같은 걸로 요청해서 결과를 되돌리면 되는 거다. 에디트 컨트롤은 단일 버퍼 기반 구조여서 텍스트 오프셋 계산은 다행히 훨씬 쉬우니까 말이다.

물론 이것은 에디트 컨트롤이 native하게 TSF를 지원하는 것보다는 효율이 훨씬 떨어지며, 오동작의 여지도 많다. 그냥 이런 발상도 가능하다는 걸 시연해 보이는 데 의미가 있을 뿐. 지금도 있는지 모르겠는데 과거에 TsfAPP라는 예제 프로그램이 바로 이걸 구현한 프로그램이었는데, 버그도 많았다.

다음 둘째로는, Windows Vista부터는 응용 프로그램이 아닌 그 밑에서 돌아가는 IME가 특수하게 요청할 경우에 한하여 에디트 컨트롤을 TSF 지원 모드로 바꿔 동작시키는 기능이 도입되었다. <날개셋> 한글 입력기는 TSF 인터페이스가 지원될 경우 기능 활용의 폭이 훨씬 더 넓어지는 관계로, 이 기능을 실제로 사용하는 옵션이 있다.
이 모드를 사용하면 한글 입력 중일 때 cursor가 깜빡이는 네모가 아니라 일반 블록 색깔로 바뀐다. 그리고 에디트 컨트롤을 특수하게 조작하는 프로그램에서 잠재적으로 오동작이 발생할 가능성도 생긴다.

이 TSF 확장 기능은 에디트 컨트롤뿐만 아니라, legacy 리치 에디트 컨트롤(=자체적으로 TSF를 지원하지는 않는 구버전)과 IE 웹브라우저 엔진이 제공하는 에디트 컨트롤에도 그대로 적용된다. 즉, 마이크로소프트가 구현한 표준 에디트 관련 컨트롤에는 거의 다 적용되므로 사용의 폭이 넓은 편이다.

이상이다.
옛날에 MSDN에서 Kyle Marsh 아저씨가 16비트 Windows 3.x 기준으로 에디트 컨트롤의 모든 기술적 디테일을 미주알고주알 늘어 놓은 글을 본 기억이 난다. 얘는 동작 방식과 규격이 20년 전이나 지금이나 완전히 굳어져 버려서 호환성 유지 차원에서 더 바꿀 수가 없고, MS가 신기술 투자는 리치 에디트에다가 집중적으로 한다고 보면 얼추 맞겠다. 그래서 리치 에디트에는 드래그 & 드롭도 있고, 하이퍼링크를 밑줄로 표시해 주는 기능도 있지만, 일반 에디트는 그런 거 없다. Ctrl+Bksp를 눌렀을 때 단어 단위로 지우는 기능 역시 리치 에디트급 이상에만 있다.

아울러, 일반 에디트는 우클릭하면 표준 메뉴가 나타나는 반면, 리치 에디트는 우클릭했을 때 제공되는 기본 메뉴가 없고 그걸 전적으로 사용자의 customization에 맡기고 있다는 차이가 있다.

Posted by 사무엘

2014/05/29 08:40 2014/05/29 08:40
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/968

요즘 운영체제들에 GUI 셸이 없는 물건은 없고, GUI에는 그림보다도 먼저 문자를 찍는 기능이 반드시 필요하다. 옛날에는 그런 출력 기능이 겨우 비트맵 글꼴밖에 지원되지 않았지만, 오늘날은 트루타입(TTF)이라고 불리는 규격의 윤곽선 글꼴이 세계를 평정한 지 오래다(오픈타입은 TTF의 superset에 해당함). 심지어 재래식 비트맵 글꼴이 필요하다 해도 일단 TTF 방식으로 저장하고서 출력한다.

게임처럼 완전 독자적인 GUI 노선을 가는 프로그램이 아닌 이상, 거의 모든 응용 프로그램들은 운영체제가 제공하고 운영체제에 기본으로 설정되어 있는 글꼴만을 사용하여 글자를 출력한다. 새로운 글꼴을 받아서 설치하는 건 사용자의 몫이다. 그러나 가끔은 응용 프로그램이 직접 글꼴을 설치해서 써야 하는 경우도 있다.

워드 프로세서 같은 오피스 프로그램이라면 운영체제 전체에 새로운 글꼴을 번들로 제공할 수 있다. 이건 global한 글꼴 추가이다. 한편, 자기 프로그램 내부에서만 특수한 custom 글꼴을 추가해서 쓰는 건 local (private)한 글꼴 추가이다.

윤곽선 글꼴 출력 엔진은 힌팅과 캐싱 기능이 곁들여진 일종의 고성능 범용 단색 벡터 그래픽 엔진이다. 그렇기 때문에 다른 프로그램들에 노출되지 않는 local 글꼴도 용도가 매우 다양하다. 수식이나 악보에서 쓰이는 비문자 기호를 찍는 건 물론이고, ‘입꼴 워드’처럼 자기만 사용하는 특수한 문자를 찍을 때도 전용 글꼴을 활용하면 된다.

당장 운영체제 자신도 이걸 잘 활용하고 있다. 테마가 도입되기 전에 Windows 창에 달린 사각형 모양의 최소화(_)/최대화/닫기(X) 그림은 글꼴 출력이고, Visual Studio 같은 데서 창을 도킹시키는 주사기/핀 모양의 그림도 글꼴이다. 아마 본인이 옛날에 블로그 글에서 언급한 적이 있을 것이다.

Windows 8은 부팅 시나 작업 중일 때 다섯 개의 구슬이 동그란 궤도를 그리면서 슝슝 돌아가는 애니메이션이 출력되는데, 이것도 애니메이션 GIF나 플래시 같은 기술이 아니라 글꼴 출력이다~! 구슬이 싹 들어갔다가 나오기도 하는 게 은근히 복잡하며, 이 애니메이션은 무려 100수십 프레임에 달한다. 유니코드 PUA 영역에다 미리 계산된 각 프레임의 모양을 그려 넣은 뒤, 그 글자를 순서대로 찍은 것이다. 놀랍지 않은가?

보급이 아닌 싸제 글꼴의 등록 및 해제를 위해 Windows는 AddFontResource와 RemoveFontResource라는 간단한 함수를 제공한다. 인자로는 등록하거나 해제하고 싶은 글꼴 파일의 경로만 달랑 주면 된다. '일단은' 말이다. 그러다 나중에는--Windows 9x 라인 말고 2000에서부터-- 두 종류의 함수가 더 추가되었다.

첫째, 바로 저 두 함수의 이름 끝에다 Ex가 붙은 버전이다. Ex 버전은 인자를 두 개 더 받는데, 하나는 아직 reserved 상태니 별 의미가 없고, 다른 하나는 사소한 비트 플래그들이다. 등록하는 이 글꼴을 시스템 전체가 아니라 우리 프로세스 내부에서만 사용하게 하는 FR_PRIVATE 옵션, 그리고 글꼴의 접근 가능 여부를 떠나서 일단 이게 EnumFontFamilies(Ex)에서 집계가 되지 않게 하는 FR_NOT_ENUM 옵션이다. 즉, 이 글꼴의 독특한 이름을 아는 프로그램만 이 글꼴을 사용할 수 있게 되는 것이다.

그리고 둘째로, 글꼴을 파일 이름이 아니라 아예 메모리 상의 데이터로 받는 AddFontMemResourceEx도 추가되었다. 이 함수로 추가되는 글꼴은 파일로 실체가 존재하지도 않고 특정 프로세스의 주소 공간에 매여 있으므로 극도로 private하며, FR_PRIVATE|FR_NOT_ENUM 속성이 언제나 선택의 여지 없이 붙는다.

요컨대 글꼴을 좀 더 가볍게 private 형태로 추가하는 기능은 Windows 2000에 와서야 새로 도입된 셈이다. 여담이지만, 이것 말고도 Windows 2000은 9x/NT4 시절에 비해 프로그램의 국제화 수준이 크게 강화된 첫 버전인지라 다국어 IME와 complex script를 포함해 글꼴을 저수준에서 조작하는 API들도 크게 추가되었다.
트루타입 글꼴의 테이블 데이터를 있는 그대로 뽑아 내는 GetFontData라든가, 글꼴이 지원하는 문자 집합을 유니코드 번호로 얻어 오는 GetFontUnicodeRanges도 이때의 산물임.

뭐 그건 그렇고 다시 글꼴 등록 얘기로 돌아오자면..
local/private 말고 전통적인 global한 글꼴 추가도 여전히 필요한 절차이다.
그런데 문제는 이게 사실 함수 호출만 한다고 완전히 끝나는 게 아니라는 것이다. 절차가 생각보다 굉장히 지저분하며 문서화가 제대로 돼 있지 않다.

1. global 글꼴은 Windows\Fonts 디렉터리에 있어야 한다. 결국 파일을 복사해 넣어야 하는데, 이 디렉터리에 read가 아닌 write를 하려면 관리자 권한이 필요하다.

2. Fonts 디렉터리에 복사된 파일을 상대로 AddFontResource(Ex) 함수를 호출한다.

3. 이 글꼴이 다음 부팅 때에도 제대로 인식되게 하려면, 글꼴 리스트를 레지스트리에다가도 등록해 줘야 한다. 위치는 다음과 같다.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
과거 9x 시절에는 Windows NT 대신 그냥 Windows이고. 저 레지스트리도 read가 아닌 write를 하려면 관리자 권한이 필요하다.


레지스트리에 등록하는 형식은 대충 보면 짐작할 수 있지만, 문제는 이 뻔한 패턴의 작업을 자동으로 대행해 주는 함수가 없다는 것이다. 등록하고자 하는 TTF 파일을 직접 파싱해서 name 테이블에 있는 이름을 얻어 와야 하나? ActiveX 컨트롤을 등록해 주는 regsvr32 유틸리티처럼 글꼴을 명령 프롬프트에서 바로 설치하거나 제거하는 유틸리티도 운영체제에 있어야 할 것 같다.

옛날에는 트루타입 글꼴을 설치하려면 CreateScalableFontResource 같은 이상한 함수도 호출해서 ttf에 대응하는 *.fot 파일이라는 걸 만들어야 했던 모양이다. 완전 불편하기 그지없는데 지금은 그럴 필요까지는 없는 듯. 20년 전 엄청 옛날의 Windows 3.1 시절에는 ttf/fot 파일 쌍이 필요했지만 95 이후로는 그런 건 없다.

반대로 이 글꼴을 제거하려면 먼저 RemoveFontResource(Ex)를 호출해 주고, 이게 성공하면 레지스트리 제거와 파일 제거를 수행하면 된다.
그런데 Windows는 파일 자체를 가상 메모리 주소 공간에다 직통으로 대응해서 쓰는(MMF) 걸 좋아하는 운영체제인지라, 시스템 공용 파일을 지우기가 더럽게 까다로운 운영체제다. 글꼴도 예외가 아니어서 파일 삭제는 access deny 에러가 뜨면서 잘 되지 않을 수도 있다. 이때는 MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT)을 줘서 다음 재부팅 때라도 파일이 삭제되게 플래그를 주면 될 것이다.

local과는 달리 global 글꼴 등록과 삭제는 이렇게 번거로운데, 게다가 관리자 권한까지 필요하니 더욱 번거롭다.
관리자 권한은 한 프로세스가 필요한 때만 잠시 사용자의 동의 하에 취득했다가 반납하는 게 없다. 애초에 자기 프로그램을 더 높은 권한으로 재실행해야 한다.
잠시 다음 상황을 생각해 보자.

  •  어떤 일을 하는 동안에도 GUI는 매끄럽게 반응하고, 작업이 취소 가능하거나 진행 상황 같은 걸 별도로 표시해야 하는 경우: 작업 부분을 별도의 스레드로 떼어 내야 한다.
  • 다른 프로세스를 훅킹해서 정보를 얻어 오거나 실행을 조작해야 하는 경우: 훅 프로시저는 반드시 별도의 DLL로 만들어야 한다. DLL은 32비트와 64비트를 모두 신경 써서 만들어야 하니 더욱 번거롭다.

그리고,

  • 평소에는 일반 모드로 실행되지만, 잠시 관리자 권한을 얻어 와서 민감한 디렉터리나 레지스트리의 내용을 변경해야 하는 경우: 그 부분만 별도의 EXE(프로세스)로 만들어서 실행해야 한다. 물론 나 자신을 특수한 인자를 주고 재실행하는 것도 괜찮다.

참고로 권한이 낮은 프로그램은 권한이 높은 프로그램에다 메시지를 못 보낸다. 그러니 프로그램 간의 통신 메커니즘도 잘 생각해 봐야 한다. =_=;;

어지간하면 골치아플 일 없이 단일 모듈, 단일 스레드만으로 모든 일을 처리하고 싶은데 그럴 수가 없는 상황이 이렇게 요약되었다.
프로세스, 스레드, DLL이 시나리오별로 다 등장했다. 글꼴 설치는 '프로세스' 분리가 필요한 작업인 것이다.

Posted by 사무엘

2014/05/26 08:23 2014/05/26 08:23
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/967

MFC 프로그래밍 잡설

1. IE 웹브라우저 윈도우 삽입

내 프로그램에다가 로컬이든 웹이든 HTML 페이지 내용을 표시해야 할 일이 생겼다. 이 경우 가장 간단한 해결책은 Internet Explorer 웹브라우저 윈도우를 삽입하는 것이다.

그런데 얘는 ActiveX 컨트롤이다. 흔히 웹페이지 내부에 들어가는 각종 ActiveX 컨트롤들이 웹 표준을 위배하고 사용자 접근성을 저해한다는 식으로 말이 많지만, 사실은 Window의 웹브라우저 자체부터가 ActiveX 형태로 제공되는 컴포넌트인 것이다. 그리고 이미 다들 아시겠지만, 플래시도 기술적으로는 ActiveX이다. 단지 이건 너무 전세계적으로 널리 퍼진 관계로 반쯤 웹 표준인 것처럼 인정받고 있을 뿐이다. (뭐, 이것도 HTML5의 등장으로 인해 지위가 좀 위태로워지긴 했지만)

어쨌든 이런 구조적인 차이로 인해, 웹브라우저 윈도우는, 리치 에디트 같은 여느 custom control과는 달리 CreateWindowEx 함수에다가 클래스 이름만 달랑 넘겨 준다고 선뜻 만들 수 있는 물건이 아니다.
MFC에서 ActiveX 컨트롤을 생성하는 코드를 보면 CWnd::CreateControl로 내려가는데, 내부 메커니즘은 각종 COM API가 동원되며 미치도록 복잡하다. 사실, 난 MFC의 도움 없이 API만으로 ActiveX 컨트롤을 생성해 본 적이 없으며, 요즘 같은 세상에 굳이 그래야 할 필요도 없을 것이다.

예전에 비주얼 C++에는 Component Gallery라는 게 있어서 (1) 스플래시 윈도우나 '알고 계십니까' 팁 대화상자처럼 몇몇 자주 쓰이는 MFC 클래스를 프로젝트에다 자동으로 등록해 주는 템플릿, (2) 그리고 특정 ActiveX 컨트롤에 대한 wrapper 클래스를 자동 생성해 주는 기능이 있었다. 6.0의 이후 버전부터는 그런 걸 못 본 것 같다.
(1)은 그렇다 쳐도 (2)는 해당 ActiveX 컨트롤의 type library를 참고하여 이 컨트롤을 생성하는 함수, 그리고 걔가 원래 제공하는 속성과 메소드들을 그대로 C++ 클래스 형태로 옮겨 주는 기능이다. CWnd의 파생 클래스인 것은 두 말할 나위도 없고.

Component Gallery가 없으니 요즘 (2)를 수행하려면 좀 우회 경로를 가야 한다. 대화상자를 하나 만든 뒤 거기서 우클릭하여 원하는 ActiveX 컨트롤을 삽입하고, 그걸 또 우클릭하여 클래스를 추가하면 된다.

다른 것도 아니고 IE 웹브라우저 윈도우는 굉장히 유명한 ActiveX 컨트롤인 관계로, 사실은 MFC에도 이미 전용 클래스가 준비되어 있다. 바로 CHtmlView 되시겠다. 이름에서 알 수 있듯 얘는 CWnd가 아닌 CView로부터 상속을 받아서 MFC의 view-document 아키텍처에 최적화되어 있다.
즉, 대화상자의 여느 컨트롤들과는 달리 스택이 아닌 heap에 생성되고, PostNcDestroy 함수에 delete this가 구현되어 있다. 그래서 대화상자 같은 데에서 간단히 사용하기에는 어려움이 있다. (뭐, 불가능한 건 아니다. 대화상자 위에다 아예 CView를 만들지 말라는 법도 없으니)

한편, CHtmlEditCtrl이라는 클래스도 있다.
IE 윈도우는 단순히 HTML을 표시만 하는 게 아니라 위지윅 HTML 편집기 기능도 갖추고 있다. 얘는 IE 윈도우를 viewer가 아닌 editor 모드로 열어 준다.
IE가 여러 모로 리치 에디트 컨트롤과도 경쟁 구도가 된 듯하다. 물론 리치 에디트가 훨씬 더 빠르고 가볍지만, 텍스트에다 서식을 입히는 데 RTF보다야 HTML이 압도적으로 더 유명한 대세가 된 건 부인할 수 없다. 그래서 도움말조차 RTF 기반인 재래식 HLP는 진작에 밀려 사라지기도 했고 말이다.

이 CHtmlEditCtrl은 CView가 아닌 CWnd 기반이다. 그래서 CDialog 파생 클래스에다가 멤버로 선언하여 대화상자의 child control로도 비교적 쉽게 사용할 수 있다. view 버전은 CHtmlEditView와 CHtmlEditDoc이 따로 있는 듯.

하지만 에디트 기능이 없는 일반 IE 윈도우를 CWnd를 기반으로 간단히 스택에다가 생성하는 건 여전히 MFC의 기존 클래스로 가능하지 않은 것 같다. 그래서 본인은 그냥 ActiveX 컨트롤 type library로부터 CWnd 파생 클래스를 추출한 후 그걸 사용하는 재래식 방법을 동원했다.

2. MFC 액셀러레이터 버그(?)

Windows API에는 메뉴 단축키를 자동으로 처리해 주는 액셀러레이터라는 게 있다. MFC에서는 CFrameWnd::LoadFrame 함수에서 자기 프레임 윈도우 ID값에 해당하는 액셀러레이터를 불러들인다.

그런데 거기에 있는 단축키를 좀 수정하고, 메뉴에다 새로운 기능을 추가하여 단축키도 액셀러레이터 테이블에다가 배당했는데, 아무리 수정을 해 줘도 새로운 단축키가 동작하질 않고 단축키가 예전 방식으로만 동작한다.
혹시 액셀러레이터 리소스가 잘못 빌드됐나 싶어서 빌드된 EXE 파일의 내부 리소스를 살펴보기도 했지만 딱히 이상이 없다.

그렇다고 해서 해당 리소스를 아예 지워 버리면 모든 단축키가 먹통이 된다. 그러나 리소스가 있으면 단축키가 있는 그대로 인식되지 않는다. 어찌 된 영문일까?

이것은 비주얼 C++ 2008 이후부터 도입된 일명 feature pack의 추가 기능 때문에 벌어지는 현상으로, 엄밀히 말해 버그는 아니다.
알다시피 MFC feature pack에서는 CWinApp, CFrameWnd 같은 전통적인 클래스에 Ex가 붙었고, MS Office처럼 프로그램의 모든 기능의 단축키를 customize하는 기능이 추가되었다. 그래서 한번 프로그램을 사용하고 나면, 그 뒤엔 프로그램이 리소스에 있는 액셀러레이터 테이블을 참조하는 게 아니라 레지스트리에 저장된 단축키를 따라 동작하게 된다. CKeyboardManager라는 클래스를 보신 적이 있을 것이다.

그렇기 때문에 프로그램 개발 과정에서 새로운 메뉴 명령이나 단축키가 추가되어 이를 테스트하고 싶다면, 프로그램을 실행한 후에 Customize 대화상자를 꺼내서 단축키를 reset시키면 된다. 아니면 해당 레지스트리를 수동으로 날리거나 레지스트리를 날리는 코드를 추가해 주면 된다. 이에 대한 자세한 정보는 구글링하면 다 나온다.

단축키와 도구모음줄을 싹 다 customize하는 기능이 필요할 정도로 규모가 방대한 프로그램을 개발할 일은 사실 그리 많지 않다.
그러니 그냥 옛날처럼 feature pack 기능을 사용하지 않는 아주 간단한 프로그램만 만들고 싶은데 요즘 MFC 마법사는 그냥 선택의 여지가 없이 Ex 클래스만 사용하여 코드를 생성해 주는 듯하다.

요즘은 MFC DLL은 이제 ansi 버전은 기본 배포조차 안 해 준다고 하지?
그나저나 (1) DLL의 덩치가 커져도 너무 커진 것, 그리고 확장팩이 그나마 MS Office나 Visual Studio의 UI를 정확하게 고증하여 재연한 것도 아니고 (2) 동작 방식이나 글꼴, 색상이 들쭉날쭉 차이가 나면서 짝퉁 티가 팍팍 나는 것을 생각하면...
MFC의 변화 양상에 대해서 본인은 불만이 좀 있다. -_-;;

예전에도 말했지만, (1)은 걍 운영체제의 내장 mfc42.dll을 직통으로 사용하는 classic legacy 모드 같은 거 좀 넣어 주면 안 되나 싶고,
(2)는.. 운영체제의 보급 메뉴 말고 싸제 메뉴가 흔히 저지르는 실수 하나만 좀 지적하고 넘어가겠다. 업계 관계자가 내 글을 보게 될 가능성은 별로 없지만..;;

메뉴가 튀어나왔을 때는 프로그램이 자체적으로 IME를 꺼야 한다. 그래서 한글 모드일 때도 Alt를 누르지 않고 그냥 누르는 메뉴 항목에 대한 단축키(액셀러레이터 키)가 먹혀야 한다. 그 글쇠가 안 먹히고 화면 한 구석에 ㅇ, ㅂ 같은 조합 윈도우가 튀어나오는 건 프로그램의 버그이다.
이것도 MS 오피스의 싸제 메뉴는 처리를 한 반면에, 요즘 MFC가 라이선스한 싸제 메뉴는 그런 처리도 안 돼 있다. 보면 볼수록 품질이 실망스럽다. 아니, Visual Studio조차도 MS Office 라이브러리가 아니라 WPF 기반으로 새로 제작된 2010 이후의 IDE는 메뉴에 저 버그가 존재한다.

<날개셋> 한글 입력기야 MFC를 사용하지 않고, 그나마 타자연습은 나온 지 10년도 더 된 구닥다리 Visual C++ 2003을 아직도 사용하며 빌드되고 있다. MFC의 배포 방식과 덩치 때문에 업그레이드를 할 처지가 못 돼서 말이다. 아니면 차라리 WTL 같은 더 가벼운 프레임워크로 갈아타야 되나 싶다.
위의 두 아이템들은 내 개인 프로젝트가 아니라 회사 일을 하면서 발견하고 느낀 것들을 글로 옮긴 것이다. 이것 말고도 기억에 남는 게 좀 있는데.. 마저 나열하면서 글을 맺도록 하겠다.

3. ShowWindow(SW_HIDE) 하니까 창이 없어져 버렸던 것. 동일한 영역의 창에 IE ActiveX 컨트롤과 여타 윈도우를 상황에 따라 교대로 보이거나 숨기는 UI를 만들 일이 있었다. 그런데 프로그램이 자꾸 이상하게 동작하고 assertion failure가 나기에 디버깅을 해 봤더니, 이게 웬걸, IE 윈도우를 ShowWindow(SW_HIDE)를 해서 숨기는 순간 컨트롤 자체가 완전히 파괴되고 m_hWnd 값이 NULL이 되는 것이었다.

검색을 해 보니 이것은 아주 잘 알려진 문제. 처음에 Create로 생성을 할 때 WS_VISIBLE가 지정되지 않았던 IE 컨트롤은 나중에 또 ShowWindow를 통해 숨겨질 때 내부 로직에 의해 destroy되어 버리는 모양이었다.
이 문제를 피해 가려면 그 윈도우에 대해서 MFC의 CWnd::ShowWindow를 호출하지 말고 그냥 Windows API 함수를 쓰면 된다고 한다. 내부 사정은 알 수 없는 노릇. 스레드를 사용할 때 이래로 MFC 클래스 대신 Windows API의 사용이 강제되는 또 다른 상황을 만났다.

4. 내 프로그램에다 삽입시킨 IE 컨트롤로 각종 자바스크립트를 사용하는 웹페이지에 접속을 하다 보면.. 스크립트 오류가 난다. gmail만 해도 로그인을 하고 나면 동일 증상을 확인할 수 있음.
이것은 IE가 보안 때문에 취한 조치인 듯하다.
HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION

요 key를 만들어서 그 밑에 이름은 "자기 프로그램.exe"이고 데이터는 10진수로 IE 버전 곱하기 1000 (=0이 3개 붙은)인 REG_DWORD를 집어넣어 주면 된다.

5. MFC 라이브러리와 표준 C++ 라이브러리를 같이 사용한 상태로 프로그램을 static link 형태로 빌드하고 나면..
operator new/delete가 중복 정의되었다고 링크 에러가 나는 경우가 있다. (DLL link는 상관 없음)
이 역시 구글링을 하면 정보가 곧바로 걸려 나올 정도로 잘 알려진 문제이다. 귀찮지만 라이브러리를 링크하는 순서를 좀 바꿔 주면 해결 가능하다. 구체적인 해결책은 지금 이 개인용 컴퓨터에 들어있지 않아서 설명을 생략하겠다. -_-

Posted by 사무엘

2014/05/20 08:18 2014/05/20 08:18
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/965

GetMessage는 Windows 프로그래밍에서 윈도우 message loop을 구현할 때 쓰이는 함수이다.
이 함수는 명목상 리턴값이 BOOL이며, 평소에는 nonzero를 되돌리다가 WM_QUIT가 접수되어서 응용 프로그램이 종료되어야 할 때 FALSE가 된다.

그러나 이 함수의 리턴값이 이것이 전부가 아니다.
정상적으로 한 메시지를 끄집어 왔을 때는 nonzero이긴 한데 양수이며, argument가 올바르지 않다거나 해서 함수의 실행 자체가 실패했을 때는 음수 -1을 되돌린다.

그렇기 때문에 메시지 loop을 while( GetMessage(&msg, NULL, 0, 0)) { }  이런 식으로 구현하면, 메시지를 아예 가져오질 못했는데도 loop의 조건이 만족되며 프로그램은 무한 루프에 빠진다.
!=0으로는 불충분하니, 반드시 while( GetMessage(&msg, NULL, 0, 0) >0)이라고... >0을 명시해야 한다.

(1) 이 함수 말고도 타입이 BOOL인데 사실은 TRUE/FALSE라는 순수한 흑백 논리값 말고 다른 의미 있는 값도 되돌리는 페이크 BOOL 함수가 또 있었던 것 같으나, 당장은 기억이 안 난다. 이런 지저분한 이슈도 있고, 또 Windows API의 기반 언어인 C가 어지간한 건 그냥 machine word 정수로 처리하는 관행이 있기도 하니(문자 상수의 크기도 char이 아닌 int!), 프로그래밍에서도 BOOL은 C++의 bool이 아니라 그냥 int에다 대응시켜 놓은 것 같다.

(2) COM에도 이와 비슷한 얘깃거리가 있다. HRESULT는 원래 0과 양수가 '성공'을 나타내고, 음수가 실패를 나타낸다. 하지만 현실에서는 대부분 그냥 hr==S_OK (0) 여부만으로 성공/실패 여부를 판단한다.
거의 모든 COM 인터페이스 함수들은 실행이 성공했을 때 어차피 S_OK라는 단일한 값만을 되돌리기 때문에 이것이 현실에서 당장 크게 문제가 되지는 않는다. 그러나 원칙적으로는 어지간해서는 hr==S_OK를 쓸 곳에 SUCCEEDED(hr)을 써야 한다. 이것은 hr>=0 여부를 체크하는 매크로이다. hr!=S_OK를 대신해서는 FAILED(hr)이 바람직하고 말이다.

음수도 아니고 0도 아닌 대표적인 리턴값은 S_FALSE이다. 이것은 해당 함수가 의미 있는 동작을 하지는 않았지만 어쨌든 오류가 발생했거나 실패한 상황도 아닐 때 돌아온다. 가령, 뭔가 객체를 enum하고 있는데, 포인터가 이미 끝에 도달해서 더 fetch할 게 하나도 없으면 보통 &ulFetched는 0이 돌아오고 함수 리턴값은 S_FALSE가 된다. 하나라도 fetch된 게 있으면 S_OK이고 말이다.
따라서 이 경우, loop의 종료 조건을 지정하려면 SUCCEEDED와 더불어 fetch된 개수도 체크해야 한다.

(3) 다시 GetMessage 얘기로 돌아온다.
얘는 메시지를 수집하는 윈도우, 그리고 필터링할 메시지의 최소값과 최대값을 인자로 받는다. 하지만 PeekMessage도 아니고 GetMessage에다가 뭔가 동작의 범위를 제한하는 유의미한 값을 지정하는 것은 사실상 거의 쓸데없는 짓이다. 언제나 NULL, 0, 0을 하는 게 맞다. (레이몬드 챈 선생도 인증한 사실임)

이 함수는 뭔가 메시지를 얻을 때까지 실행이 끝나지 않고 계속 기다린다. 어떤 GUI 프로그램이 실행되면 굳이 자신이 아니어도 그 스레드 소속으로 남이 생성한 각종 잡다한 윈도우가 붙는다. 이들 윈도우도 메시지 큐로부터 메시지를 받아야 하는데, GetMessage에다가 필터링을 걸면 해당 윈도우는 메시지를 받지 못하며 그 동안 우리 프로그램도 실행되지 못하게 된다. 쉽게 말해 deadlock에 빠진다.

따라서 아무 윈도우로 전달된 아무 메시지라도 일단은 받아서 윈도우 프로시저로 Dispatch를 시켜야 한다. 정 특정 메시지만 필터링을 하고 싶다면 아까도 말했듯이 PeekMessage를 쓰는 게 훨씬 더 안전하고 바람직하다. 얘는 그래도 한 번만 체크 후 실행이 곧장 끝나기라도 하니까 말이다.

Posted by 사무엘

2014/04/30 08:31 2014/04/30 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/957

Windows API에서 LoadLibrary는 잘 알다시피 자기 프로세스 공간에다가 다른 DLL을 추가로 매핑시키는 매우 중요한 함수이다. 코드 실행이 아니라 단순 리소스 추출만이 목적인 경우, EXE 내지 현재의 기계가 지원하지 않는 다른 아키텍처로 빌드된 모듈을 열 수도 있다.
Windows에서 어떤 모듈(EXE/DLL)은 타 모듈을 말 그대로 자신이 실행되는 로드타임(load-time) 때 곧장 불러와서 여타 DLL에 대한 함수 링킹을 할 수 있으며, 반대로 실행 중인 런타임(run-time) 때 동적으로 할 수도 있다.

로드타임 로딩은 간편하긴 하지만 모듈 파일 이름 이상으로 불러들이고자 하는 디렉터리 위치를 세밀하게 제어할 수 없다. 그리고 모듈이 단 하나라도 존재하지 않거나 해당 파일에 함수 심벌이 단 하나라도 존재하지 않으면 프로그램이 전혀 실행되지 않는다. 그런 예외 상황이 발생했을 때의 제어도 사용자가 전혀 할 수 없기 때문에 프로그램의 유연성 내지 융통성이 떨어진다는 단점이 있다. 내 프로그램의 로드 자체가 실패해 버리니 말이다.

그래서 새 운영체제에 들어있는 API 함수를 쓰긴 하는데, 그 함수가 존재하는지 체크를 미리 해서 구형 운영체제와의 호환성도 유지하기 위해서는.. LoadLibrary와 GetProcAddress를 이용하는 런타임 로딩 기법을 써야 한다. 로드타임 로딩이라면 운영체제가 그 기능을 내부적으로 자동으로 제공해 줬을 텐데 런타임 로딩은 함수 포인터를 수동으로 관리해야 하니 좀 번거롭긴 하다.

이럴 때, 비주얼 C++ 6때부터 도입된 delay-loading은 로드타임 로딩과 런타임 로딩의 장점을 취합한 굉장히 괜찮은 대안이 될 수 있다. COM은 런타임 로딩을 규격화된 인터페이스라는 형태로 좀 더 깔끔하게 정형화한 바이너리 표준일 테고 말이다.

DLL을 불러올 때, LoadLibrary에다가 파일의 절대 경로를 주지 않고 파일 이름만 달랑 넘겨 주면 이 함수는 프로그램이 실행된 current 디렉터리, 프로그램이 존재하는 디렉터리, 운영체제의 시스템 디렉터리, PATH에 등록된 디렉터리 등 다양한 순서대로 파일을 탐색한다. 로드타임 로딩 때도 대상 DLL은 이런 순서대로 탐색된다.

이것은 프로그램의 동작에 유연성을 부여하고 한 DLL을 여러 프로그램들 사이에서 최대한 쉽게 공유가 되게 하기 위함이나, 오늘날에 와서는 이런 정책은 보안에 악영향을 끼치게 되었다. 이름만 같고 우선순위가 더 높은 디렉터리에 존재하는 악의적인 DLL이 잘못 선택되어 로딩될 수 있기 때문이다. 또한 하위 호환성이 지켜지지 않는 시스템 DLL이 덮어써짐으로써 DLL hell 같은 고전적인 문제도 야기될 수 있다. 갈수록 난잡해지고 포화 상태로 치닫는 시스템 디렉터리는 큰 골칫덩어리이다.

LoadLibrary의 디렉터리 탐색 방식 자체를 바꿀 수는 없다. 그 방식에 의존하여 동작하는 수많은 기존 프로그램들과의 호환성을 유지해야 하기 때문이다. 그렇기 때문에 오늘날 마소에서는, 새로 개발되는 프로그램에서는 LoadLibrary를 호출할 때 저런 알고리즘에 의존하지 말고 반드시 DLL의 전체 경로를 명시해 줄 것을 권고하고 있다. 또한, 전면 금지하는 건 불가능하겠지만, 운영체제의 시스템 디렉터리에다가는 가능한 한 자기 싸제 DLL을 집어넣지 말 것을 권고한다.

일례로, 예전에 qt 라이브러리를 DLL 링크한 프로그램을 돌려 봤다. 이 DLL은 운영체제의 시스템 디렉터리에 있는 것도 아니었는데 어떻게 DLL을 찾는지 궁금했는데... 에구, 그냥 PATH 지정이더라. 운영체제의 환경변수를 대놓고 바꿔 버리는 건 굉장히 안 좋은 방법인데 어쩔 수 없다. 이제 윈도 XP가 나온 지도 10년이 넘었으니 WinSXS 매니페스트 방식이라도 써야 하지 않나 싶다.

요컨대 Windows는 (1) 제3자 응용 프로그램이 자기끼리 공유하는 공용 DLL을 손쉽게 찾아 로딩하는 법, 그리고 (2) 운영체제의 시스템 DLL을 보안 문제 없이 간편히 로딩하는 법을 완전히 이원화하여 체계적으로 제공할 필요가 있다. 확장 버전인 LoadLibraryEx 함수에라도 그런 기능을 좀 추가할 수 없나 궁금하다.

(1)의 경우 아까도 말했듯이 delay-loading이나 WinSXS가 그럭저럭 해결책이다. 이게 중요한 문제이기 때문에 윈도 XP부터는 SetDllDirectory 함수가 추가되긴 했으나, 이것은 로드타임 로딩을 제어할 수 없기 때문에 여전히 근본적인 해결책이 아니다.

(2)는 known DLL 리스트라는 게 레지스트리에 존재한다. 그래서 "kernel32", "user32" 같은 건 DLL계의 예약어(reserved word)처럼 돼서 주변에 kernel32.dll, user32.dll 같은 동일 명칭의 사칭 파일이 있다 해도 언제나 운영체제의 시스템 디렉터리에 있는 놈이 로딩된다는 게 보장된다.

그러나 개인적인 생각은 그걸 API 차원에서 좀 더 엄밀하게 했으면 좋겠다. 아까 말했듯이, API 하위 호환성 유지를 위해 운영체제의 시스템 DLL을 수동으로 로딩하는 경우는 빈번하게 존재한다. 그리고 운영체제가 버전업되면서 새로운 시스템 DLL이 앞으로 얼마나 더 추가될지 모른다. 그러니 오로지 시스템 DLL만 로딩하고 다른 싸제 DLL은 유사품이 있다 해도 거부하는 명령이 있었으면 좋겠다.

앗싸리 시스템 DLL까지 몽땅 시스템 디렉터리 전체 경로를 지정해 줘서 로딩시켜 보기도 했다. 그런데 이렇게 했더니 한 가지 복병에 걸리는 게 있다. 바로 comctl32.dll이다.
얘는 WinSXS 매니페스트로 넘어가서 길고 이상한 다른 디렉터리에 있는 놈이 로딩되기 때문이다. 이걸 무시하고 운영체제 시스템 디렉터리에 있는 놈을 로딩하면 TaskDialog처럼 공용 컨트롤 6.0 이상에서만 지원되는 기능들을 사용할 수 없다.

그러니 시스템 DLL은 경로 다 생략하고 그냥 "comctl32", "kernel32" 이렇게만 로딩하는 게 정답인 듯하다.
문득 스프레드 시트 프로그램인 엑셀이 생각난다. 엑셀은 구조적인 이유로 인해 이름이 동일하고 서로 다른 디렉터리에 존재하는 여러 파일을 동시에 열 수 없다. 수식에서 다른 워크시트 파일을 참조할 때 디렉터리 대신 오로지 파일 이름만을 토대로 탐색을 하기 때문이다.

Posted by 사무엘

2014/04/27 08:25 2014/04/27 08:25
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/956

1. 특정 명칭(클래스, 함수, 변수 등등)의 선언지로 곧바로 찾아가기.
(1) 소스 코드에서 cursor나 마우스 포인터가 가리키고 있는 명칭에 대해서는 현재 소속되어 있는 클래스나 namespace 문맥을 감지하여 동작해야 하며, (2) 그냥 임의의 심벌을 타이핑하여 조회하는 기능도 있어야 한다. 둘 다 필요하다.

2. 디렉터리를 불문하고 프로젝트에 있는 특정 파일 이름을 곧바로 타이핑으로 조회하여 파일 열기. 시작하는 단어와 중간에 있는 단어가 모두 지원되어야 한다.

3. 그리고 명칭이 아닌 임의의 문자열을 검색하는 Find in files인데, 다음과 같은 범위에서 모두 가능할 것.
(a) 소스(=번역 단위)든 헤더든 프로젝트에 정식으로 등록돼 있는 파일
(b) 프로젝트에 정식으로 등록은 안 돼 있지만, 등록된 파일로부터 인클루드에 의해 한 번이라도 엮이는 파일들
(c) 프로젝트 파일이 하나라도 존재하는 디렉터리에 덩달아 있는 모든 소스 파일들

즉, 3은 파일 내부의 문자열 검색이고 2는 파일 이름 자체의 검색이다. 2의 경우 일단은 검색 도메인이 (a)만으로 한정이지만, 2도 (b)나 (c)가 옵션에 따라 지원된다면 금상첨화다.

Visual Studio IDE의 경우, 1은 진작부터 인텔리센스 엔진을 통해 지원되어 왔다. 그러나 2는 2012에 와서야 가능해졌으며, 3은 (a)만 가능하다. (c)를 하려면 결국 프로젝트 경로를 수동으로 직접 입력해야만 가능하여 매우 불편함. 프로젝트에 존재하지는 않지만 같은 디렉터리에 있는 파일들을 덩달아 찾아야 할 때도 있는데도 말이다.

물론 (b)는 소스 코드를 컴파일까지는 아니어도 전처리기 수준의 파싱은 해야 구현 가능하기 때문에, 좀 어려울지 모른다. #include를 제대로 처리하려면 프로젝트 차원의 인클루드 디렉터리 관리자가 있어야 하며, 조건부 컴파일뿐만 아니라 인클루드 대상 자체에 대해서도 매크로 상수 전개가 필요할 때가 있으니 말이다.

c/cpp 같은 소스 코드가 그 자체로 온전한 번역 단위를 구성하는 게 아니라, 다른 소스 코드에 또 인클루드되어 쓰이는 경우가 있다. 물론 프로젝트에 등록되지 않은 채로 말이다.
이런 파일은 (a) 형태의 문자열이나 파일명 검색이 되지도 않아 대단히 불편하며, IDE가 구문 분석을 하는 것도 굉장히 복잡하고 어렵게 만든다. C/C++에서 인클루드는 정말 양날 달린 검인 게 실감이 간다.

끝으로 (b)와 관련된 여담 하나 좀 남기겠다.
과거 비주얼 C++ 6 시절엔 프로젝트 파일 리스트에 External dependencies라고 해서, 정식으로 프로젝트에 포함돼 있지는 않지만 프로젝트 파일에 의해 인클루드되는 파일을 대충, 얼추 계산해서 표시해 주는 기능이 있었다. '대충, 얼추'라는 말은 그 동작이 100% 정확하지는 않았다는 뜻이다. 그러던 것이 닷넷으로 넘어가면서 이 얼렁뚱땅 불완전한 기능은 삭제되었다.

그 뒤, 버전이 201x으로 넘어가면서 이 기능은 부활했다. 온전한 컴파일러가 소스 코드를 머리부터 발끝까지 다 분석하면서, MFC와 플랫폼 SDK가 중첩 인클루드하는 수십, 수백 개의 헤더 파일들을 하나도 빠짐없이 정확하게 나열해 주는 무시무시한 기능으로 다시 태어난 것이다. 비주얼 C++ IDE는 변화가 없는 것 같아도 내부적으로 이렇게 변모하고 있다.
모든 파일들의 의존도 정보를 파악하고 있다는 소리이니, 이를 바탕으로 함수 호출 tree처럼 파일들의 include 계층 다이어그램(includes / included by)을 그려 주는 기능은 IDE에 혹시 없나 궁금하다.

Posted by 사무엘

2014/04/21 08:28 2014/04/21 08:28
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/954

Windows API에는 FreeLibraryAndExitThread라는 함수가 있다.
얘가 하는 일은 이름이 암시하는 바와 같다. 주어진 모듈에 대해 FreeLibrary를 한 뒤 ExitThread를 호출하여 지금 실행 중인 자기 스레드를 종료한다.

어지간하면 응용 프로그램이 두 함수를 차례로 직접 호출해 주면 되며, 둘을 나란히 호출해야 할 일 자체도 실무에서는 매우 드물다. DLL을 제거하는 건 대체로 응용 프로그램이 종료될 때 같이 행해지지, 특정 스레드의 실행이 끝나자마자 칼같이 행해지는 일은 없기 때문이다. 그런데 왜 이런 함수가 정식으로 별도로 제공되는 걸까(Windows NT 3.1 첫 버전은 아니고 3.5에서 추가됨)? 다 이유가 있기 때문이다.

어떤 DLL이 별도의 스레드를 생성해서 자신의 코드를 동시에 실행하고, 실행 후에는 자기 자신을 스스로 깔끔하게 제거하여 사라지게 설계되어 있다고 치자. 즉, DLL을 로딩한 모듈이 그걸 수동으로 따로 해제하는 게 아니라, 그 DLL이 자기 임무를 다한 후 스스로 사라진다는 것이다. 이건 비록 흔한 디자인 형태는 아니지만 말이다.

이 경우, DLL의 코드가 자신에 대해서 FreeLibrary(hMyDLL)을 함부로 호출해 버려서는 곤란하다. 그러면 자기 스레드가 활동하던 모듈이 메모리에서 사라져 버리기 때문에 그 다음에 실행될 스레드 실행 종료 부분에 해당하는 코드(return 0 내지 ExitThread)까지 사라진다. 스레드는 정상적으로 종료되지 못하고 곧장 access violation이 발생한다.

그렇다고 ExitThread를 먼저 호출하면? 이 DLL을 실행하는 주체인 스레드의 실행이 끝나 버리기 때문에 FreeLibrary가 호출될 기회가 없다. 마치 실행 중인 EXE 자신을 제거하는 코드를 실행하는 것과 비슷한 맥락의 딜레마가 발생한다.

모듈(공간) 해제와 스레드(시간) 종료가 동시에 행해져야 할 경우, 이것은 운영체제가 알아서 동시에 수행해 줘야 한다.
FreeLibraryAndExitThread를 호출한 경우, 내부적으로 FreeLibrary가 됐더라도 그 다음으로 ExitThread를 호출하는 코드는 그 DLL이 아니라 운영체제가 책임을 지기 때문에 안전하다.

한편, Windows에서는 EXE나 DLL의 로딩이 파일 자체를 가상 메모리 주소에다 곧장 연결하는 memory-mapped file 기법으로 행해진다. 그렇기 때문에 실행 중인 프로그램 파일이 중간에 지워지는 것은 운영체제가 허용하지 않는다. 즉 DLL과 스레드 문제로 비유하자면 FreeLibrary 단계에서 이미 실패한다는 것이다.

그렇다고 자신을 먼저 종료해 버리면 자기 자신 파일을 지우는 코드가 실행되지 못할 테니, 이 역시 동시에 충족될 수 없는 모순이 된다. ExitProcessAndDeleteFile 같은 전용 함수라도 있어야 할 것 같은데.. Windows API에는 그런 건 없다.

다만 EXE와는 달리 실행 중인 배치 파일은 자기 자신을 제거하는 게 가능하다. 그래서 설치 제거 프로그램 같은 데서는 내부적으로 EXE를 지우고 자기 자신도 삭제하는 배치 파일을 만들어서 이걸 내부적으로 실행한 뒤, 자기 프로그램은 최대한 신속하게 종료함으로써 흔적 없는 '자폭'을 한다. 배치 파일은 당연히 IF EXISTS와 GOTO loop가 있어서 파일이 완전히 지워질 때까지 삭제 시도를 반복한다.

배치 파일은 자가삭제가 가능하긴 하지만 삭제된 뒤의 명령들은 실행되지 못하고 에러와 함께 실행이 끝난다. 옛날에 4DOS 같은 MS-DOS 대체 명령 프롬프트에는 BTM (Batch to memory)처럼 모든 내용을 메모리로 읽은 뒤에 실행하는 특수한 배치 파일이 있긴 하지만 이것은 MS 기반의 오리지널 셸에 있는 기능은 아니다.

물론 설치/제거 프로그램이라면 요즘은 직접 짜는 게 아니라 Windows Installer를 사용하는 게 대세이니 저런 꼼수를 직접 구현해야 할 필요는 더욱 없어지긴 했다. 자기 정체를 요리 조리 교묘하게 숨겨야 하는 악성 코드나 돼야 필요할까?

거의 모든 Windows API 함수들은 뭘 실행하더라도 성공/실패 여부를 되돌리는 리턴값이 있다. 리턴값이 없는 void형 함수는 정말 실패를 절대로 걱정할 필요가 없는 예외적인 물건을 제외하면 매우 드물다.
그러나 ExitProcess 내지 ExitThread처럼 뭔가 자신의 실행을 종료하고, 실행 후에 리턴값을 받을 주체 자체가 없어지는 함수라면 리턴값이 응당 void이다. 종료하는 동작이 실패할 리도 없을 테고 말이다.

FreeLibraryAndExitThread도 예외가 아니다. 모듈 핸들을 잘못 줄 경우 FreeLibrary 부분은 실행이 실패할 수 있지만, 후반부의 ExitThread 부분은 언제나 성공이 보장되기 때문이다.

그러고 보니 메시지 큐에다가 WM_QUIT를 넣어 주는 PostQuitMessage도 종료와 관계가 있는 전용 함수이며 void 형이다. WM_QUIT 메시지는 GetMessage 함수로 구성된 message loop을 끝내는 역할을 하며, 윈도우 프로시저를 통해 전달되지는 않는다.
MSDN은 저 메시지는 반드시 PostQuitMessage라는 전용 함수를 통해서(주로 메인 윈도우의 WM_DESTROY 타이밍 때) 넣어야지, 수동으로 PostMessage(hWnd, WM_QUIT, 0, 0) 같은 식으로 넣어서는 안 된다고 명시한다.

사실 저건 특정 윈도우가 대상이 아니라 스레드의 메시지 큐 자체가 대상이기 때문에 hWnd에 아예 NULL을 주거나, PostThreadMessage(GetCurrentThreadId(), WM_QUIT, 0, 0)과 비슷하다.
다만, WM_QUIT은 여느 메시지와 동일하게 취급되는 게 아니라 스레드의 내부 상태 차원에서 종료 플래그가 붙은 것으로 특수하게 처리되기 때문에 PostQuitMessage 같은 별도의 함수가 존재하는 것이다. 제거 딜레마의 해결 필요 때문은 아니다.

Posted by 사무엘

2014/04/15 08:25 2014/04/15 08:25
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/952

소프트웨어 GUI 구성요소 중에 콤보 박스는 굉장히 유용한 물건이다.
공간을 적게 차지하면서 리스트 박스의 역할을 고스란히 수행할 수 있으며(비록 다중 선택은 안 되지만)
에디트 박스(입력란)의 역할을 수행하면서, 예전에 사용자가 입력했던 문자열이나 샘플이 될 수 있는 디폴트 값을 곧장 선택할 수 있게도 해 주기 때문이다.

입력란이 없이 선택만 가능한 타입을 Windows에서는 drop list 형태라고 부른다.
일반적으로 콤보 박스에서는 마우스 휠을 굴리거나 상하좌우 화살표를 누르면 선택 항목이 인접한 다른 것으로 바뀐다. 그리고 마우스를 클릭하거나 키보드 F4 내지 Alt+상하 화살표를 누르면 리스트가 화면에도 뜬다. 이것이 표준 동작 방식이다.

그런데 Windows는 이것과 약간 다른 형태의 동작 방식도 지원한다. 일명 extended UI라고 들어 보셨나 모르겠다.
이 모드를 사용하는 콤보 박스는 일반적인 기능은 여느 drop list 콤보 박스와 완전히 똑같다.
그러나 얘는 일단 마우스 휠에 반응하지 않는다. 클릭을 하거나 아래 화살표를 누르면 먼저 리스트부터 나타난다. 그 뒤에야 상하 화살표나 마우스를 이용해서 선택 아이템을 변경할 수 있다.
다른 프로그램들에서 이렇게 동작하는 콤보 박스를 본 기억이 있으신 분 계신가? 이런 콤보 박스는 매우 드물고 보기 힘들긴 할 것이다.

extended UI가 지정된 콤보 박스는 아이템 선택을 바꾸는 게 번거롭다. 굳이 리스트를 꺼내지 않고 간편하게 선택을 바꿀 수가 없으며, 리스트를 여는 키보드/마우스 동작이 반드시 선행되어야 하기 때문이다.
굳이 이런 모드를 왜 넣었는지 본인으로서는 알 길이 없다. 혹시 Windows 3.x 시절에는 콤보 박스가 원래 이렇게 동작하기라도 했었나? 아니면 다른 프로그램의 GUI와 호환성을 유지하기 위해서였는지?

호환성이 이유라면 차라리 모든 콤보 박스들이 extended이든 그렇지 않든 사용자가 지정한 방식으로 동일하게 동작하도록 제어판 설정 같은 걸 추가하면 된다. 굳이 각각의 콤보 상자에 따로 적용되는 옵션으로 둘 필요는 없다.

더구나 extended UI의 사용 여부는 내가 아는 한은 어느 개발 환경에서도 리소스 차원에서 개체 속성의 수정만으로 간단히 지정하는 방법이 없다. 다시 말해 반드시 코딩을 통해서 지정해 줘야 하며, API의 형태도 구리다는 뜻이다. 이상한 점이 아닐 수 없다.

CB_GETEXTENDEDUI 메시지를 보내서 사용 여부를 얻어 오고, CB_SETEXTENDEDUI로 지정하면 된다. 진짜로 BOOL 값 하나를 달랑 얻어 오거나 지정하는 get/set 함수 형태 그 이상도 그 이하도 아니다.

트리 뷰나 리스트 뷰 같은 공용 컨트롤의 경우, 지정할 수 있는 속성이 워낙 많다 보니 운영체제가 기본으로 할당해 주는 스타일과 확장 스타일(extended style)로도 공간이 부족하다. 그래서 자체적인 추가 확장 스타일을 얻거나 지정하는 메시지가 따로 존재한다.

그러나 콤보 박스는 필요한 정보량이 겨우 1비트에 불과하고 그 정도면 그냥 CBS_(EX)_EXTENDEDUI 같은 스타일 비트 하나만 추가하는 걸로 충분하지 않았을까? 안 그래도 잉여력이 충만한 옵션인데 왜 사용하기도 번거롭게 만들어 놓았나 하는 의문이 남는다. 아니... 반대로 잉여로운 옵션이니까 API도 그렇게 따로 뚝 고립시켜 놓은 것인지도?

옛날에는 extended UI를 리소스 에디터에서 속성 변경만으로 곧장 지정했던 것 같기도 해서 지금 비주얼 C++의 리소스 에디터를 뒤져 보고, 심지어 비주얼 C++ 6 같은 옛날 버전들도 살펴봤다. 하지만 그런 건 없다.

그 대신, 비주얼 C++ 4~6이 사용하던 옛날 프로퍼티 대화상자가 사용하는 콤보 박스가 extended UI를 사용하는 것을 확인했다. 마우스 휠이 동작하지 않으며, F4 대신 아래 화살표를 누르면 drop list가 나오더라.
그리고 그러고 보니.. MS 오피스 프로그램들, 특히 대화상자들까지 운영체제의 표준 GUI 대신 자체 GUI를 쓰는 Word와 Excel의 경우, 모든 콤보 상자들이 extended UI 기반이긴 하다.

운영체제의 GUI를 API 날것 형태로 그대로 제공하는 게 아니라 자체적으로 재분류도 해 놓은 닷넷(C#)이나 비주얼 베이직 6의 폼 에디터도 살펴봤다. 콤보 박스를 집어넣었지만 딱히 extended UI 속성을 지정하는 프로퍼티는 보이지 않는다. (단, 델파이까지는 못 살펴봄)

이상, Windows의 기본 GUI에 존재하는 어느 대단히 잉여로워 보이는 기능과 불편한 API에 대해서 살펴보았다.
지금이나 앞으로나.. extended UI가 적용된 콤보 상자는 거의 찾을 수 없을 것이다.
옛날에 노턴 유틸리티의 GUI(뭐, 텍스트 모드에서 GUI를 비슷하게 흉내 내며 동작한 것이니 정확히 말하면 TUI?)가 제공하는 콤보 상자는 Ctrl+아래 화살표를 눌러야 리스트가 떴었는데 Windows는 F4 또는 "Alt+아래 화살표"이구나.

여담을 하나 남기며 글을 맺겠다.
운영체제가 제공하는 각종 Win+? 단축키에 대한 설명은 컴퓨터 활용 팁 같은 데에 많이 소개되어 있다. 하지만 운영체제가 제공하는 기본 컨트롤 자체에 대한 팁 정보는 상대적으로 문서화나 공유가 덜한 것 같다.

예를 들어 복수 선택이 가능한 리스트 컨트롤은 Shift+F8을 눌러서 복수 선택 모드로 진입하게 되어 있는데, 이것 말고도 내가 모르는 기능이 있는지도 모른다. 왜 하필 그냥 F8도 아니고 Shift+F8인 것이며 그 유래는 무엇일까?
그리고 리스트 뷰 컨트롤은 아이템 이름을 바꾸는 단축키가 왜 하필 F2일까? 이런 것에 대해 좀 더 알았으면 하는 생각이 있다.

Posted by 사무엘

2014/04/04 08:31 2014/04/04 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/948

#define의 대체제

확실히 #define은 다른 걸로 대체 가능할 때는 가능한 한 안 쓰는 게 좋을 것 같다.
C++은 용도별로 다음과 같은 다양한 대체제를 제공한다.

1. 매크로 함수의 대체제: 인라인 함수로 대체 가능하며, 템플릿까지 동원하면 매크로 함수 만만찮은 유연한 메타프로그래밍이 가능하다.
또한 한 함수 안에서만 지엽적으로 반복되는 루틴을 정리하려면 C++0x부터는 람다 함수를 쓸 수도 있다.

2. 매크로 상수의 대체제: 정수의 경우 enum을 쓰면 같은 성격의 여러 심벌들을 한데 묶어 놓을 수도 있어서 좋다.
그리고 문자열은 그냥 const char/WCHAR 형태의 전역/클래스 static 변수로 처리함. 선언과 정의가 따로 존재해야 해서 불편할 수 있으나, 이것은 선언부에다 값을 다 집어넣고 확장 문법인 __declspec(selectany) extern const 를 지정해서 해결할 수도 있다.

아무 통제도 없이 너무 일방적으로 효력이 나타나는 #define보다는 저런 대체제들이 type-safety와 엄격한 scope 검증이 보장되기 때문에 "훨씬 더" 깔끔하다. 가능한 한 전처리기보다는 컴파일러에게 일을 맡기는 게 바람직하다.
내가 만든 명칭이 매크로로 이미 존재하여 딴 걸로 치환되고 있는 줄도 모르고 컴파일러가 자꾸 이상한 난독증을 보이며 에러를 뱉는 것 때문에 빡친 경험이 있는 사람.. 주변에 의외로 많다. ㅎㅎ

단, 그럼에도 불구하고 대체제가 존재하지 않아서 #define을 불가피하게 써야만 하는 경우는 아마도 다음과 같을 것이다.

1. #if #elif #endif 같은 조건부 컴파일 변수 지정

2. 함수 형태를 갖추기조차 민망할 정도로 너무 간단한 로직. 디버그 빌드에서도 독립된 함수 호출이 아니라 언제나 인라이닝이 반드시 보장되기를 바라는 부분

3. 호출하는 함수나 지정하는 변수 이름을 말 그대로 간단히 치환만 시키기를 원하는 경우

4. 대체제의 문법적 한도를 넘는 과격한 구문 치환을 해야 하는 경우. 특히 #나 ## 같은 연산자를 동원해서 완전히 새로운 토큰을 만들어 내야 할 때

5. __LINE__, __FILE__, __TIME__ 같은 빌드/디버그 정보를 그때 그때 삽입하고 싶을 때

6. 정수와는 달리 부동소숫점과 문자열은 여전히 #define이 유용한 경우가 있다.
부동소숫점은 enum이 지원되지 않고 static const 멤버도 클래스 선언부에서 바로 값 지정이 되지 않기 때문이다. (이걸 지원하는 컴파일러도 있긴 하나, 일단은 비표준임)
문자열은 매크로 상수의 경우, concatenate(연결)되는 문자열의 일부가 되는 게 가능하다. const 상수는 그렇지 않다.

#include와 #define이 너무 지저분하고 컴파일 시간을 증가시키는 요인이라며 없애자니.. 위와 같은 용도까지 부정하는 건 현실적으로 무리이긴 하다.

여담으로..
근래엔 남이 만든 코드를 읽다가 IID_PPV_ARGS라는 매크로를 보고 감탄하여 내가 짠 기존 코드에다가도 다 리팩터링을 해서 적용해 놨다.

CoCreateInstance와 IUknown::QueryInterface 때 꼴도 보기 싫던 void ** 형변환을 없애 주는 매우 편리하고 유용한 물건이다. COM이 등장한 건 무려 20년이 넘었고 C++에 템플릿이 추가된 것도 만만찮게 오래 됐을 텐데 이 매크로는 무려 Windows 7의 플랫폼 SDK에서야 정식 등장했다는 게 놀랍다.
매개변수 2개를 하나로 줄이는 역할까지 하니 이 정도라면 컴파일러가 아니라 전처리기 매크로밖에 선택의 여지가 없긴 하다.

Posted by 사무엘

2014/04/01 19:20 2014/04/01 19:20
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/947

윈도 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 사무엘

2014/02/26 19:27 2014/02/26 19:27
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/935

« Previous : 1 : ... 15 : 16 : 17 : 18 : 19 : 20 : 21 : 22 : 23 : ... 32 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2026/10   »
일 월 화 수 목 금 토
        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:
4192515
Today:
751
Yesterday:
1684