1. Windows 명령 프롬프트에서의 UTF-8 지원

본인은 1년 남짓 전에 UTF-8 인코딩에 대해서 글을 쓰면서 Windows도 콘솔 환경에서 UTF-8이 지원되는 날이 올까 썰을 푼 적이 있었다.
이건 Windows 10이 업데이트를 거치면서 어느 정도 현실이 됐다. chcp 65001이 가능해졌다. 명령 프롬프트와 배치 파일에서 깨지는 문자열 걱정 없이 파일 이름이나 각종 메시지를 지정할 수 있다는 것은 좋은 소식임이 틀림없다.

사실, 콘솔 자체는 문자의 특정 인코딩에 전혀 구애받지 않고 임의의 바이트 나열을 주고받을 수 있는 '파일'일 뿐이다. 그러니 콘솔의 코드 페이지가 437이건 949건 65001이건 전혀 상관없이 프로그램은 모조리 생까고 아무 인코딩으로나 유니코드 문자열을 printf 같은 함수로 출력할 수 있다. 가령, chcp 949 상태이더라도 consoleapp.exe > output_in_utf8.txt 는 깨지는 문자 없이 얼마든지 가능하다는 것이다.

다만, C# 같은 언어로 콘솔 프로그램을 만들어서 System.Console.WriteLine을 해 보면, 닷넷 프레임워크가 현재 콘솔의 코드 페이지 번호에 따라(아마도 GetConsoleOutputCP 함수) 문자열을 1바이트 단위 기반으로 변환해서 출력한다.
그러므로 chcp 949 상태에서 듣보잡 한자를 찍으면 깨진 문자열이 아니라 완전히 정보가 소실된 ?로 바뀌어 찍히며, 이는 >를 이용해 파일로 redirect하더라도 마찬가지이다. chcp 65001을 해 줘야 이런 손실이 없어진다.

상황이 이러한데.. 현재 운영체제는 새로운 셸인 PowerShell 말고 기존 명령 프롬프트에 대해서는 chcp 65001이 그냥 가능은 하다는 선에서만 지원을 멈춘 듯하다. 하위 호환 문제 때문에 아예 처음부터 UTF-8를 디폴트로 구동시키지는 못하는 것 같다. 새로운 콘솔 프로세스를 생성해서 거기 코드 페이지를 65001로 변경하는 작업도 불가능하지는 않지만 꽤 번거롭고 지저분하다. Windows의 콘솔 API들은 자기 자신의 코드 페이지를 변경하는 것만 직통으로 지원하기 때문이다.

하긴, 437이니 949니 하는 ANSI 코드 페이지라는 건 유니코드를 지원하지 않는 레거시 앱들과의 호환 때문에 존재하는 개념이다. 그러니 시스템의 기본 코드 페이지가 65001이더라도 저런 디폴트 fallback용 코드 페이지는 계속해서 존재해야 할 것이다.
배치 파일은 @echo off로 시작하는 게 거의 관행인데, @를 거의 메타커맨드 식별자화해서 앞에 @encoding 이런 식으로 인코딩을 지정하는 것도 아주 황당한 생각은 아닐 것 같다.

더 궁극적으로는 굳이 콘솔에만 국한되지 않더라도, Windows API에서 A 함수와 W 함수는 UTF-8이냐 UTF-16이냐의 차이밖에 없어지는 날이 올까? UTF-8을 native로 지원한다는 표식이 된 프로그램에 한해서라도 말이다.
물론 요즘 만들어지는 프로그램이야 처음부터 W 함수만 쓰겠지만.. 타 플랫폼에서 1바이트 문자열 기반으로 유니코드를 지원해 온 프로그램을 포팅할 때는 저런 접근 방식도 분명 도움이 될 것이다.

레거시 프로그램들과의 호환은.. 지금 마소에서 high DPI 지원을 위해서 기를 쓰고 제공하는 샌드박스 가상화 계층만 만들면 얼마든지 가능하지 않겠나 개인적으로 생각한다. manifest 파일에 native UTF8을 지원한다고 명시해 주는 식으로 말이다.

2. 메뉴 간소화

기능이 너무 많아서 메뉴가 "파일, 편집, 보기, 도구"부터 시작해 10개를 훌쩍 넘어가고, 각 카테고리별로 항목들이 20개 가까이 주렁주렁 달린 방대한 프로그램을 생각해 보자. 사용자에게 굉장히 위압적으로 보일 것이다.

더구나 이런 메뉴들은 static한, 고정된 기능 나열 목록일 뿐이다. 파일 내지 글꼴 같은 다이나믹한 목록이 아니며, 사용자가 자주 사용하는 기능을 곧장 고를 수 있게 해 주는 것도 아니다. 프로그램의 모든 기능을 골고루 다 쓰면서 지내는 사용자는 당연히 극소수이고 말이다.

사정이 이러하니 이런 압박스러운 메뉴를 어찌해 보려는 노력이 먼 옛날부터 있었다.
자주 쓰는 기능과 그렇지 않은 기능을 프로그램 개발사에서 답정너 분류한 뒤.. 메뉴를 반토막 간소화해서 보여주는 옵션을 갖춘 프로그램의 예로는.. 도스 시절 MS QuickBasic IDE, 그리고 아래아한글 2.1과 그 이후 버전이었다. 메뉴에서 사라진 기능이라 해도 단축키로는 물론 상시 접근 가능하다.

아래아한글의 경우, 워디안/2002 이전의 3.x~97에도 간소화 메뉴 옵션이 있었던가 그건 잘 모르겠다.

마소에서는 딱 2000년대 초에 일명 Personalized menu라는 걸 도입했다. 사용 빈도를 동적으로 체크해서 오랫동안 안 쓰인다 싶은 메뉴는 감춰 버리고, 메뉴를 꺼낸 지 시간이 좀 오래 지나거나 맨 아래의 ▼을 눌러야만 메뉴가 마저 다 나오게 했다.
Windows 2000/ME의 시작-프로그램 메뉴, 그리고 MS Office 2000/XP/2003 정도에서 이런 메뉴를 볼 수 있었다.

사용자 삽입 이미지

이건 마소의 입장에서는 많은 시간과 비용을 들여 진행한 실험이었으며, 실제로 메뉴 첫인상을 좀 단순하게 보이게 하는 효과가 있었다.
하지만 사용자의 반응은 썩 좋지 않아서 최종적으로는 FAIL 판정을 받았다. Office 길잡이 같은 급의 병맛 흑역사는 아니었지만 몇 년 못 가 없어졌으며, 결국 2000년대 후반부터는 리본 인터페이스가 도입되었다.

참고로 Visual Studio IDE는 일반인이 쓰는 프로그램이 아니어서 그런지.. MS Office의 UI 엔진을 그대로 쓰던 시절에도 Personalized menu가 쓰인 적이 없었다. macOS에도 저런 게 도입된 적은 없지 싶다.

3. macOS

한편, macOS는..

  • 응용 프로그램 패키지(.app)는 하위 디렉터리와 그 밑의 여러 파일들로 구성돼 있음에도 불구하고 Finder에서는 단일 파일인 것처럼 취급해 준다.
  • 그 반면, zip 압축 파일은 그냥 단일 파일 형태임에도 불구하고 내부에 들어있는 하위 디렉터리와 파일들을 실제 파일과 다를 바 없이 seamless하게 표시해 준다.

없는 디렉터리를 있는 것처럼 표시하고, 반대로 있는 디렉터리를 숨기는 가상화를 구현하느라 애 많이 썼을 것 같다.
app 패키지도 안드로이드 apk처럼 그냥 압축 파일 컨테이너에다 넣어 버리지 싶은 생각이 든다. 성능 문제 때문에 그렇게 하지 않은 건지?

그리고 macOS는 GUI 응용 프로그램은 app이고 콘솔 프로그램은 그런 껍데기 없이 a.out 실행 파일인 건가? 일반 실행 파일과 달리 app은 표준 C 함수 말고 NSWorkspace 소속의 코코아 API를 써서만 실행할 수 있는 거고? (내부의 하드코딩된 경로에 일반 실행 파일이 있긴 함) 그 관계를 잘 모르겠다.

내 경험상 mac은 프로세스 간 통신이 Windows보다 제약이 심하고 폐쇄적인 것 같다. 특히 훅킹 같은 게 없기 때문에 macOS용으로 Spy++ 같은 프로그램을 만들 수는 없는 듯하다.
게다가 한 프로그램이 다른 프로그램으로 키 입력을 보내는 게 요 1, 2년 전쯤 시에라에서부터 막혔다. 사용자가 제어판을 열어서 동의를 찍은 프로그램에서만 가능하다.

이런 이유로 인해 macOS에서는 날개셋 편집기나 외부 모듈 같은 프로그램은 만들어도 날개셋 입력 패드 같은 구현체는 만들 수 없겠다. 또한 외부 모듈에서 키 입력을 보내는 날개셋문자도 구현할 수 없겠다. =_=;;

4. 맥 계열과 타 PC와의 차이

(1) 같은 파일이 컴퓨터를 옮기니까 크기가 몇십 MB씩 차이가 나 있어서 깜짝 놀랐는데..
알고 보니 Windows는 전통적으로 1024 기반의 MiB 단위를 쓰는 반면, 맥은 그냥 깔끔하게 1000 단위로 자리수를 매겨서 발생한 차이였다.

(2) 그러고 보니 맥OS는 메시지 박스에서의 버튼 배치 방식도 Windows와 다르다. '예'에 해당하는 1순위 버튼이 색깔도 파랗게 강조된 형태로 대화상자의 맨 오른쪽 아래에 있다. 이건 뭐 그냥 디자이너의 취향 차이인 것 같다.

(3) 일반적으로 키보드의 좌측 하단에는 modifier key들이 ctrl, fn, win, alt의 순으로 배치돼 있다. 그러나 애플 제품들은 맥북/아이맥 구분 없이 다 fn, ctrl, alt, cmd(win)의 순이기 때문에 1234가 2143으로 기묘하게 바뀌어 있다.
서로 알력 싸움에 따로 노는 건지는 모르겠지만, 두 종류의 컴퓨터를 드나드는 사람의 입장에서는 이게 불편하기 그지없다..;;

5. 안드로이드와 iOS

요즘 전세계에 보급된 스마트폰들에서 안드로이드와 iOS의 점유율이 적게는 7:3, 많게는 4:1 가까이 된다고 한다.
이 둘을 더하면 진짜 99.99%이고, Windows Phone 등 타 운영체제는 이제 유의미한 점유율을 완전히 상실한 듣보잡이라고 한다. 마소에서도 Windows 기반의 자체 모바일 운영체제는 이미 포기하고 접었고..

오픈소스 진영에다가 구글· 삼성의 막강한 지원 덕분에 안드로이드는 확실한 주류가 됐다. 하지만 그렇다고 iOS가 무시 가능한 처지인 건 아니다.
이건 마치 전세계 도로의 우측통행 vs 좌측통행의 점유율과 비슷한 구도인 것 같다. 내 기억으로 저것도 비율이 3:1 내지 4:1쯤이지 싶다.

좌측통행이 분명 비주류이긴 하지만 그렇다고 극소수인 건 아니며, 영국· 일본· 오스트레일리아 등 존재감을 절대로 무시할 수 없는 강대국 선진국들이 엄연히 좌측통행을 하고 있다. 그러니 완전히 없어질 수가 없다.
이런 좌측통행의 점유율과 위상이 마치 오늘날 iOS의 그것과 비슷하게 느껴진다는 것이다. 오래된 생각이다. (CPU 업계에서 리틀 엔디언과 빅 엔디언의 점유율 비율은 어찌 되었나 궁금해지기도 한다만..)

그러고 보니 세벌식도 여전히 1%가 채 안 되고, 내가 혼자 생각하는 것보다 훨씬 더 지위가 약한 마이너이구나..;;
더구나 지금 세벌식을 쓰는 사람들도 자기 자식에게까지 차마 세벌식을 권하지는 않겠다고 다짐하는 경우가 적지 않다. 이걸 생각하면 일면 우울해진다. 타자가 편리한 것보다 당장 생활에서 불편하고 번거로운 게 더 크게 느껴지는가 보다.
그나마 명분이 옳고 객관적으로 이론적으로 성능이 우수하기 때문에 오늘날까지도 명맥이 유지되고 있는 것이다.

Posted by 사무엘

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

1. Windows 재설치

(1) 본인 주변의 어떤 컴퓨터는 절전이나 최대 절전 말고 '시스템 종료'로 확실하게 껐는데도 불구하고 나중에 다시 와 보면 무슨 좀비처럼 저절로 다시 켜져 있곤 했다. 사람이 건드린 건 당연히 전혀 아니고..
절전 상태에서 깨어나는 게 아니라 완전히 꺼졌던 컴퓨터가 왜 어떻게 저절로 다시 켜지는지는 나로서는 도무지 이해할 수 없다. 컴퓨터를 끄는 걸로도 모자라서 플러그까지 완전히 뽑아야 되나 하는 생각마저 들었다.
이건 검색을 해 보니 2015~16년경 Windows 10 초창기에 존재했던 버그였다.

(2) Windows가 맛이 가서 제대로 부팅이 되지 않을 때 안전 모드로 부팅한다거나, 컴을 팩토리 리셋 시키기 전에 최소한 백업이라도 하기 위해서 긴급 명령창이라도 열려면 파란 배경의 Windows RE (Recovery Environment)에 들어가야 한다.
부팅 때 F8 같은 특정 단축키를 눌러서 여기로 바로 들어갈 수 있으면 좋겠구만.. 꼭 부팅 중에 컴퓨터를 강제로 끄는 무식한 삽질을 5~10번쯤 해야 그제서야 "잠시 기다려 주세요"와 함께 저리로 들어가는 게 개인적으로 굉장히 마음에 안 든다.

(3) Windows를 새로 설치한 직후에 다중 모니터를 연결해 봤는데.. 무작정 꽂기만 한다고 장땡이 아니구나. 2개 중 HDMI 케이블로 연결한 4K급 모니터는 아무리 제대로 꽂아도 화면이 나타나지 않았다. 그래픽 드라이버를 업데이트 한 뒤에야 잡히더라.

그래도 요즘 컴퓨터는 수십 년 전 옛날에 비해 새 하드웨어를 꽂아서 사용하는 게 얼마나 간편 편리해졌나 모른다. 상당수가 plug & play와 USB 덕분인데, 생각해 보니 두 기술이 동시에 같이 등장한 건 아니라는 게 의외이다.

2. 자동 시작 프로그램 목록

그나저나 Windows 10은 아마 3.x 이래로 20년 가까이 변함 없던 '시작 프로그램' 등록 지점을 변경했구나. 난 지금까지 모르고 있었다. 어쩐지 '시작프로그램'이라는 폴더가 왜 안 보이나 했다. Windows 8 시절에도 변함없었던 것 같은데..
디렉터리 기반인 건 변함없지만 그게 시작 메뉴의 목록 디렉터리 아래가 아닌 다른 곳으로 바뀐 것 같다.

운영체제가 부팅될 때 자동으로 실행시킬 프로그램의 목록은 저 디렉터리뿐만 아니라 레지스트리에도 여러 군데가 있다. 이것만 한데 정리해도 블로그 글 한 편이 써지지 싶다. 도스가 사라지면서 config.sys와 autoexec.bat가 없어졌지만, 걔네들이 하던 기능까지 없어진 건 아닌 셈이다.
리눅스나 macOS는 자동 시작 프로그램 목록을 어디에서 지정하는 걸까? 궁금해진다.

아울러, 요즘 Windows는 SMB(쌈바)가 기본으로 깔리지 않는가 보다. 랜선 꽂고 인터넷이 멀쩡히 되는데 탐색기에서 \\로 시작하는 공유 폴더 서버에 왜 이리 접속이 안 되나 했더니.. "프로그램 추가/제거"에서 해당 기능을 설치해야 했다. 인터넷 검색을 하면 '로컬 보안 정책'을 고치라니 뭐니 하는 말이 있었지만, 그걸 건드릴 필요는 없었다.

3. embedded 컴포넌트로 활용 가능한 브라우저

2010년대 이후로 웹브라우저 시장에서 IE의 독주가 꺾이고 크롬, FireFox, Edge 같은 대체 브라우저들이 많이 쓰이고 있다.
그런데 IE는 여느 브라우저들과 달리 컴포넌트화가 잘 돼 있다. 그래서 임의의 프로그램 내부에 IE 기반의 브라우저 창을 집어넣어서 단순 HTML 내지 웹 컨텐츠를 표시할 수 있다. 기술 기반은 잘 알다시피 COM/ActiveX이고 말이다.

IE 말고 다른 브라우저 창을 내 프로그램에다가 내장시키는 건 가능한지 모르겠다.
그게 가능하지 않다면 IE는 이쪽 고정 수요 때문에 계속 없어지지 않고 명맥을 유지할 것 같다.

4. 잠금 화면의 배경 그림

Windows 8(.1)까지만 해도 안 그랬던 것 같은데.. 10부터는 부팅 직후, 또는 Win+L을 눌렀을 때 나타나는 잠금 화면에 기본적으로 근사한 각종 자연 경관 배경 그림이 나타난다. 각종 업데이트를 받기에도 네트워크 트래픽이 빠듯할 텐데, 어떤 서비스는 거기에 또 짬을 내서 그림 파일도 찔끔찔끔 받아 놓는가 보다.

이 그림도 분명 디스크 어딘가에 파일 형태로 저장될 텐데, 슬쩍 빼돌리는 방법에 대해서는 이미 팁이 나돌고 있기 때문에 검색해 보면 다 나온다. 그런데 본인이 문득 궁금한 건 이 그림들을 마소의 어느 부서에서 담당하고 있으며, 누가 어디서 이런 사진들을 구해서 올리느냐 하는 것이다.
Windows와 mac 진영 모두 근사한 자연 경치 사진을 공급받는 비밀 거래처가 있는 것 같다. 그건 개인일 수도 있고 기업일 수도 있다. Windows XP의 초원 배경은 이미 아주 유명한 사례이다.

5. Windows - 설정 창의 불편한 점

요즘 Windows는 버전업을 거듭할수록 제어판은 뒷전으로 밀려나고 운영체제의 모든 설정을 메트로 앱인 '설정'으로 하게 UI를 옮기려는 게 보인다.

(1) 그런데 이놈의 설정 앱은 좀 여러 개를 띄울 수 없나? 프로그램 추가/제거 같은 창을 꺼내 놓은 상태로 디스플레이/개인 설정 같은 걸 띄우려니 기존 창이 바뀌어 버리는 게 굉장히 불편하다. 10수년 전에 탭이 없던 IE6 시절에, 응용 프로그램에서 인터넷 링크를 클릭하면 기존 브라우저 창에서 그 링크가 열려서 짜증 나던 게 고스란히 재연되고 있다.

(2) '설정'에서는 키보드의 연타 속도를 조절하는 방법이 1803 내지 1809 빌드 기준으로 여전히 존재하지 않는다. 그럼 그런 키보드 설정은 기존의 제어판에 들어가서 하라고 링크라도 띄워 놔야 되는데 그것도 없는 건 문제인 것 같다.
마우스의 경우, '설정'에서는 좌우 버튼 교환이나 휠 스크롤 분량 같은 대중적인(?) 옵션만 있다. 포인터 모양을 바꾸는 매니악한 옵션은 제어판에 가서 하라고 '추가 마우스 옵션' 링크가 표시되어 있다. 키보드는 그런 게 없고 '고급 키보드 옵션'은 그냥 IME/문자 입력 쪽 설정밖에 안 나온다.

(3) 요즘 Windows 10에서 중국어· 일본어 IME를 쓰고 싶으면 해당 언어와 키보드만 등록하는 게 아니라, 언어 팩을 다 받아서 설치해야 하는 것 같다. 전자까지만 하면 IME가 뜨긴 하지만 실질적으로 중국어· 일본어 입력이 되지 않는다.
20년쯤 전 먼 옛날에는 마소에 내장돼 있는 외국어 IME를 처음 등록할 때 운영체제 CD를 집어넣어야 했다. Vista~7에서는 그냥 전세계 언어가 다 깔렸었는데 이제는 인터넷으로 받는 형태로 절차가 바뀐 것 같다.

(4) 그리고 외국어 UI 팩이나 입력기, 필기· 음성 인식 데이터를 받고 설치하는 버튼을 실수로 잘못 눌렀는데, 작업을 취소하는 버튼을 왜 마련해 놓지를 않았는지? 이것 때문에 꽤 당혹스럽고 불편한 경험을 하기도 했다. =_=;;

6. 입력 도구모음줄의 불편한 점

오늘날 Windows 10에서는 IME에 대해서 브랜드 아이콘(한)과 상태 아이콘(가/A) 딱 둘만 작업 표시줄에 붙어 있는 극도로 단순화된 아이콘 표시 모델을 지원한다. 요건 사실 2012년의 Windows 8때부터 도입된 것이니 생각보다는 오래됐다.

지금도 과거의 XP~7 시절을 풍미했던 길쭉한 입력 도구모음줄을 쓰는 것 자체는 가능하다. 특히 키보드에 한자 키가 없거나, 그 키를 자기 멋대로 가로채는 프로그램(MS 오피스 프로그램..)에서 한글 IME의 고유 방식대로 한자 변환을 하려면 이 도구모음줄이 필수이다. 한자(漢) 버튼이 노출되어 있어서 마우스 클릭이 곧장 가능하기 때문이다.

그런데 이 도구모음줄을 꺼내는 옵션이 꽤 찾기 어려운 구석진 곳에 있다는 점은 차치하고라도.. 이 도구모음줄이 불편한 점 중 하나는 floating 상태일 때 화면에서 찾기가 어렵다는 점이 아닐까 한다. 그리고 그 이유 중 하나는 배경색이 순 화이트이기 때문이지 싶다.

이 도구모음줄은 도입된 이래로 배경색은 줄곧 작업 표시줄의 색깔과 일치해 왔다.
Windows 98/2000/ME에서는 회색, XP에서는 파랑 같은 테마 색 또는 회색(고전), 그리고 Vista/7에서는 역시 검정..
그러니 10에서도 저 패턴이라면 배경색이 작업 표시줄의 색깔과 같은 검정이어야 할 텐데, 어째 제목 표시줄의 색깔과 같은 흰색이 됐다.

제목 표시줄도 흰색이지, 프로그램의 일반 클라이언트 영역도 흰색이지, 그런데 설상가상으로 도구모음줄 주변엔 아무 테두리도 없고 그림자도 없으니.. 찾기 어려울 수밖에 없다.
역대 Windows들 중에 TSF 도구모음줄의 배경색이 흰색인 버전 자체가 Windows 10이 유일하다시피하다. 8과 8.1에서는 어땠는지 기억이 안 난다만 걔네들도 흰색은 아니었지 싶다.

하지만 이 도구모음줄은 MDI 창이라든가 HTML 도움말처럼 마소에서 더 개발이나 지원을 하지 않는 레거시일 뿐이니, 얘에 대한 개선은 앞으로 기대하기 어려울 것 같다.

7. caret의 깜빡임이 멈추는 현상

이제 단순히 불편한 점을 넘어 버그 얘기를 좀 해 보자. 내 컴퓨터만 그런 줄 알았는데 아니더라.
요즘 Windows에서 날개셋 편집기, 메모장, 워드패드처럼 운영체제가 제공하는 caret(키보드 커서)을 사용하는 프로그램을 띄워서 가만히 놔둬 보면..
caret이 딱 5번 깜빡이고 나서 6번째로 나타난 뒤부터는 깜빡이지 않고 그대로 멈춰 버린다.

Windows 10의 1803과 1809 버전에서 모두 확인했는데 이런 버그가 왜 생겼나 모르겠다. 2015~16년대의 구버전에서는 이런 일이 없었다. 아니, 수십 년 전의 Windows 95 이래로 이런 현상은 처음 본다.
Spy++로 들여다보면.. caret을 깜빡여 주라는 메시지 자체가 더 오지 않고 멈춘다. 그에 반해 아래아한글이나 MS Word 같은 전문 워드 프로세서는 이런 메시지에 의존하지 않고 자체적으로 caret을 운용해서 그런지 깜빡임이 멈추지 않는 것 같다.

8. Dependency Walker의 오동작

이전에는 이런 현상이 없었던 것 같은데.. Windows 10 1809를 설치한 뒤에는.. 각종 실행 파일을 Dependency Walker 유틸로 들여다보고 분석하는 게 다소 불편해졌다.
Windows 7쯤부터 운영체제의 자체 제공 EXE/DLL들은 kernel32.dll의 함수들을 kernel32로부터 직통으로 import하는 게 아니라 프로세스, 스레드, 파일, DLL로딩 등 각종 세부 분야로 나뉜 가상의 DLL로부터 import하기 시작했다. comctl32의 로딩 방식은 side-by-side assembly 방식으로 바뀌더니 kernel32는 저렇게 바뀌었다는 게 흥미롭다.

그런데 Windows 10 1809 버전에서는.. KERNEL32.DLL은 API-MS-WIN-CORE-PROCESSTHREADS-L1-1-1.DLL에 의존하고, 후자는 또 전자에 의존하는 구조가 돼서 여기서 일종의 무한루프에 빠진다.
비록 Dependency Walker 자체가 뻗는다거나 하지는 않지만, 이 때문에 분석 시간이 굉장히 길어지고 모듈 분석 트리도 쓸데없이 거대하고 지저분한 모양으로 만들어진다.
문제가 개선됐으면 좋겠지만 Dependency Walker는 무려 Windows Vista 초창기인 2006~7년 이후로 개발이 중단되고 버전업이 없으니 아쉽다.

Posted by 사무엘

2019/06/28 08:38 2019/06/28 08:38
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1634

Windows용 GUI 프로그램은 가시적인 GUI 구성요소인 윈도우/창이라는 걸 생성해서 화면에 띄울 수 있다.
윈도우는 메뉴, 제목 표시줄, 테두리 같은 '껍데기' non-client 영역과, 그 내부에 프로그램이 재량껏 내용을 표시하는 '알맹이' client 영역으로 나뉜다. 윈도우에다 스타일을 무엇을 주느냐에 따라 non-client 영역의 구성요소와 차지 면적이 달라진다. 똑같이 테두리를 주더라도 크기 조절이 가능한 창은 그렇지 않은 창보다 테두리가 더 두껍게 그려지곤 한다.

GetWindowRect 함수는 어떤 창이 화면에서 문자 그대로 차지하는 스크린 좌표(non-client와 client 모두 포함)를 되돌리며, GetClientRect는 창 내부의 클라이언트 영역의 크기를 되돌린다.
후자의 경우 RECT를 되돌리긴 하지만 left와 top 멤버의 값은 언제나 0으로 설정된다. 그러니 얘는 진짜 RECT라기보다는 SIZE의 성격에 더 가깝다. 하지만 창의 client 영역을 원점(0,0) 기준으로 DC에다 그림을 그릴 일이 많으며 GDI 함수 중에도 RECT를 받는 놈이 있다. 그렇기 때문에 저런 형태의 RECT를 받는 건 여러 모로 유용하다.

어떤 창의 클라이언트 영역이 자기 창의 non-client 영역을 기준으로 얼마나 떨어져 있는지, 그리고 원점 또는 전체 스크린 좌표 기준으로 어디에서 시작하는지를 얻기란 쉽지 않다.
또한, 어떤 child 윈도우가 부모 창의 클라이언트 영역 좌표를 기준으로 어디쯤에 있는지를 얻는 것도 바로 가능하지 않다(예: 대화상자의 어떤 컨트롤이 대화상자를 기준으로 어느 위치에 있는가?). 기준이 되는 창들을 모두 스크린 좌표로 얻어 온 뒤에 수동으로 좌표 오프셋 보정을 해야 답을 구할 수 있다.

뭐 이런 식이다.
크기와 스타일이 동일한 창에 대해서 전체 영역 대비 클라이언트 영역이 위치와 크기가 어떻게 결정될 것인지는 얼추 고정적으로 예상 가능하다.
그런데 이렇게 직관적이고 명백해 보이는 영역에도 뭔가 골치 아프고 지저분한 가상화, 보정, 샌드박스라는 게 존재한다는 게 믿어지시는가?

이 문제는 지금으로부터 10수 년 전, Windows Vista가 등장하면서 시작됐다.
과거의 XP가 단조로운 고전 테마를 탈피하여 non-client 영역과 각종 표준 컨트롤에 임의의 테마 비트맵을 도입했는데, Vista/7은 더 나아가서 그래픽 가속 기능의 도움으로 프로그램 창 주변에 짙은 그림자를 그리고, 테두리에는 뭔가 청명한 느낌이 나는 반투명(?) 유리 재질을 구현했다.

그래서 법적으로(?) 똑같은 크기의 창이라도 창이 화면에다 실제로 1픽셀이라도 그림을 그리는 영역이 더 커졌으며, 테두리의 두께도 더 두꺼워졌다.
특히, 크기 조절이 되지 않는 창이라도 "크기 조절이 되는 창과 동급으로" 테두리가 두꺼워졌다.
이로 인해 window rect라는 개념을 해석하는 방식에도 변화가 불가피해졌다.

그럼 골치 아픈 설명 대신 예를 들도록 하겠다.
대화상자를 생성하여 (1, 1) 위치에다 300, 200픽셀 크기로 배치(SetWindowPos 호출)하는 프로그램을 돌려 보았다.
이걸 Visual C++ 2010 이하, 또는 2012 이상이라도 Windows XP 호환 툴체인으로 빌드하면 실행 결과가 다음과 같이 나온다.

사용자 삽입 이미지

그러나 동일한 코드를 Visual C++ 2012 이상의 정규 툴체인으로 빌드하면 실행 결과가 다음과 같다. 둘의 차이가 뭘까?

사용자 삽입 이미지

옛날 방식으로 빌드하면.. window rect를 계산할 때 Windows Vista/7의 Aero가 추가적으로 그려 주는 껍데기 공간이 영역에 포함되지 않는다. 그래서 window rect가 음수가 아니라 분명 (1, 1)임에도 불구하고 창의 좌측 상단이 좀 짤린다.
이 창이 실제로 차지하는 영역을 얻으려면 DwmGetWindowAttribute(m_hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rc, sizeof(rc))를 호출해 줘야 한다. 보다시피 실제 영역은 (-4, -4)에서부터 시작하면서 5픽셀이나 더 크다는 걸 알 수 있다.

그러나 최신 방식으로 빌드하면 창의 모든 영역이 window rect에 포함된다. 이 때문에 창의 크기가 더 작아지며, 클라이언트 영역의 크기도 10픽셀씩이나 차이가 나게 된다. 그 대신 얘는 GetWindowRect와 DwmGetWindowAttribute의 결과가 서로 일치한다.
두 프로그램의 실행 결과를 한 화면에 포개 놓으면 다음과 같다.

사용자 삽입 이미지

이런 차이를 만들어 내는 것은 바로.. 실행 파일의 PE 헤더에 기록되어 있는 운영체제/서브시스템의 버전이다. 링커에서 /SUBSYSTEM:WINDOWS,6.0 옵션으로 지정 가능하다.
Visual C++ 2010의 컴파일러는 5.1 (XP)이 최저값이기 때문에 필요한 경우 6.0 (Vista)으로 올려서 지정할 수 있다. 그러나 2012 이상 후대의 컴파일러는 선택의 여지 없이 6.0이 최소값이며 더 낮은 값으로 지정이 되지 않는다. 툴체인 자체를 더 낮은 것으로 골라야 한다.

의외로 매니페스트 XML이 아니라는 게 신기하다. 공용 컨트롤 6.0 사용 여부, 고해상도 DPI 인식 여부, 요구 보안 등급 같은 건 매니페스트 속성이니까 말이다.
당연한 얘기이지만 공용 컨트롤 6.0과 최소 운영체제 6.0은 서로 다른 개념이라는 걸 유의하도록 하자. 공용 컨트롤의 버전은 차라리 과거 IE의 버전(6)과 비슷한 구도이다. 일개 웹브라우저가 운영체제의 셸까지 마개조하던 시절의 잔재이다(DLL hell도 존재했었고..).

그래도 현재의 Visual C++ 2019도 최소 운영체제 버전이 6.0에서 값이 더 오르지는 않고 있다. Vista를 진짜 하한선 마지노 선으로 잡고 있는 듯하다.
그럼 다음으로 Aero를 끄고 Basic 테마에서 두 프로그램을 실행한 결과는 다음과 같다.

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

desktop window manager가 동작하지 않으니 DwmGetWindowAttribute 함수는 실행이 실패했다.
두 경우 모두 프로그램의 외형은 짤리는 것 없이 동일하고, 테두리의 두께만이 차이가 날 뿐이다. 최신 방식으로 빌드된 프로그램은 더 두껍고, 그렇지 않은 레거시 프로그램은 얇다.

단, Aero의 동작 여부와 무관하게 최신 방식과 레거시 방식이 클라이언트 영역 크기는 동일하다는 걸 알 수 있다. 전자는 테두리가 두꺼우니 클라이언트 영역이 (284, 162)로 더 작고, 후자는 (294, 172)로 더 크다.

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

고전 테마는 Windows 8~10에서는 더 찾아볼 수도 없는 모드가 됐는데.. 얘는 빌드 방식에 따른 차이가 전혀 존재하지 않는다. DwmGetWindowAttribute도 당연히 동작하지 않고 말이다.
단지, 얘는 크기 조절 가능한 창에 대해서는 테두리의 두께가 1픽셀.. 눈꼽만치 더 늘어난다. 그래서 창의 크기가 같을 때 클라이언트 영역의 크기가 (294, 175)에서 (292, 173)으로 2픽셀씩 더 줄어드는 걸 알 수 있다.

사실은, 대화상자를 크기 조절 가능하게 만들면 6.0이 아닌 옛날 방식으로 빌드된 프로그램이라도 앞의 Aero 내지 Basic 테마에서 창이 최신 6.0 버전이 지정된 프로그램과 동일하게 동작한다.
리소스 편집기에서 대화상자의 Border 속성을 Dialog Frame 말고 Resizing으로 지정했을 때 말이다.

그럼 마지막으로 Windows의 종결자 버전인 10에서는 창이 어떻게 뜨는지 살펴보자.
Windows 10의 프로그램 창은 외관상으로 Vista/7 같은 두툼한 테두리가 없고 슬림하다. 그럼에도 불구하고 10에서도 6.0이 지정된 프로그램은 클라이언트 영역의 크기가 레거시 방식 프로그램보다 더 작게 설정된다. 다시 말하지만 GetWindowRect의 결과는 둘 다 동일한데... 이렇게 동작해야 할 이유가 없어 보이는데도 말이다.

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

더욱 괴이한 것은 DwmGetWindowAttribute의 실행 결과이다.
나름 DWM은 동작하고 있는지 이 함수도 값을 되돌리기는 하는데... 보다시피 리턴된 사각형이 GetWindowRect의 리턴값보다도 더 작다~! Vista/7의 Aero와는 결과가 딴판이다.
최신 6.0 방식으로 빌드된 프로그램은 (1, 1) 위치에서 (300, 200) 픽셀 크기를 찍더라도 실제로 생기는 창의 크기는 (8, 1)에서 (286, 193) 픽셀 크기 남짓으로 예상보다 꽤 작게 된다.

지금까지 나열한 실험 결과를 표로 한데 요약하면 다음과 같다. 96dpi (100%) 확대 배율에서 굴림 아니면 맑은 고딕의 완전 기본 metric 기준이다.
GetWindowRect의 값이 다 똑같이 [1,1, 301,201]인 윈도우이더라도 실제 외형과 클라이언트 영역 크기는 운영체제와 빌드 방식에 따라 저렇게 차이가 날 수 있다는 것이다.

  레거시 (2010/XP) 신형 (2012+/Vista+)
DWM Attribute client 크기 DWM Attribute client 크기
Vista/7 Aero [-4,-4, 306,206] [294, 172] [1,1, 301,201] [284, 162]
Vista/7 Basic N/A [294, 172] N/A [284, 162]
Classic N/A [294, 175] N/A [294, 175]*
Windows 10 [3,1, 299,199] [294, 171] [8,1, 294,194] [284, 161]

* resizable하지 않은 대화상자의 경우, 고전 테마에서는 레거시 및 신형 방식 모두 대화상자 외형 차이가 없다. 단지, resizable한 대화상자는 프레임 두께가 1픽셀 더 두꺼워져서 클라이언트 영역이 2픽셀씩 줄어든다.
그리고 고전 이외의 다른 테마에서 resizable한 대화상자는 레거시 방식도 신형 방식과 동일한 결과가 나온다.

그러니, 똑같은 소스 코드이더라도 최신 Visual C++ 컴파일러로 다시 빌드를 하면 예전보다 창 크기가 더 작게 나와서 UI 구성 요소의 오른쪽/아래쪽이 짤리는 부작용이 발생할 수 있다. 그런 상황이 발생하면 놀라지 말고 SetWindowPos 같은 함수에 숫자를 무식하게 하드코딩 하지 않았는지를 침착하게 점검해 보기 바란다.

옛날에는 SetWindowPos, GetClientRect 같은 함수의 msdn 설명을 보면 Windows Vista에서 보정으로 인한 동작 차이가 발생할 수 있다는 말이 '비고'란에 분명 있었다. DWM 쪽의 API를 참고하라는 말도 봤었는데 2010년대 이후부터는 삭제된 듯하다. 지금은 그런 말을 찾을 수 없다.

하지만 그런 말이 있건 없건, 창의 크기를 지정할 때는.. non-client 요소가 일체 없고 픽셀값을 반드시 곧이곧대로 지정해야만 하는 불가피한 상황이 아니라면 어지간해서는 SetWindowPos에다가 하드코딩된 값을 무식하게 전하는 일이 없어야 한다.
이 글에서 소개한 non-client 영역 크기 보정도 있고, 화면 확대 배율(DPI)도 있고.. 하드코딩 픽셀값을 쓸 수 없게 만드는 요인은 여럿 존재한다. 잘 만들어진 프로그램이라면 창 크기를 계산할 때 그런 변수들을 잘 감안해야 한다.

가령, 대화상자 리소스 템플릿에서 사용되는 단위인 DLU를 픽셀수로 보정하려면 MapDialogRect을 사용하면 된다. 이것만으로도 화면 확대 배율은 커버 가능하다.
그리고 특정 클라이언트 영역의 픽셀수와 정확하게 대응하는 window rect 크기를 구하려면 AdjustWindowRect(Ex)를 사용하면 된다.

Posted by 사무엘

2019/05/27 08:34 2019/05/27 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1624

Windows 운영체제에서 쓰이는 EXE나 DLL 같은 실행 파일들은 잘 알다시피 Portable executable이라는 포맷을 따라 만들어져 있다. 그래도 맨 앞에는 MZ로 시작하는 MS-DOS EXE 헤더가 호환성 차원에서 여전히 있으며, 거기서 가리키는 오프셋을 따라가 보면 그제서야 PE 헤더가 시작된다.

그런데 아무 EXE/DLL이나 찍어서 바이너리 에디터로 들여다 보시라. 도스 EXE stub과 실제 Windows용 실행 파일의 사이 공간에는 뭔가 4바이트 간격으로 규칙성이 느껴지는 정체불명의 데이터가 수십~수백 바이트가량 있으며, 끝에는 약속이나 한 듯 "Rich"라는 문자열이 있다. 이건 도스/Windows 어느 플랫폼에서도 쓰이지 않는 잉여 데이터이다.
이놈의 정체는 도대체 무엇일까...??

사용자 삽입 이미지

더욱 괴이한 것은.. 이건 Visual Basic, Visual C++ 같은 마소의 개발툴(링커)로 생성한 바이너리에만 존재한다는 것이다.
게다가 마소 개발툴조차도 처음부터 이랬던 게 아니다. Visual C++의 경우, 5.0의 마지막 서비스 팩(아마 3)부터 적용된 거라고 한다. 그러니 지금으로부터 20여 년 전, 대략 1997~98년 사이부터이고.. 그냥 편의상 6.0부터라고 생각해도 무방하다.

본인의 고딩 시절, Visual C++ 4.2로 빌드된 날개셋 한글 입력기 1.0 내지 PentaCombat 오목 게임의 실행 파일을 들여다보면.. 아니나다를까 일명 "Rich 헤더"가 존재하지 않는다. MZ와 PE 사이에 아무 공백이 없다.

사용자 삽입 이미지

그러나 VC++ 6으로 빌드된 날개셋 1.2부터는 응당 Rich 헤더가 추가되어 있다.

과거 Windows 9x의 경우, 내부의 프로그램들을 빌드할 때 Visual C++이 아닌 다른 자체 컴파일러를 사용했다. kernel32, gdi32, user32 같은 DLL이라든가 메모장 같은 간단한 프로그램 말이다.
여기서 유래된 바이너리들은 Rich 헤더가 존재하지 않으며, 이 관행은 Visual C++ 6이 출시된 뒤에도 Windows 98/ME까지 변함없이 이어졌다.

하지만 내장 프로그램 중에서 워드패드와 그림판, EUDC 편집기처럼 Program Files\Accessories에 들어있던 프로그램은 MFC도 사용하고 나름 Visual C++ 냄새가 났었다.
얘들은 Windows 95 시절엔 대외적으로 공개된 적이 없는 MFC 짝퉁을 사용하다가, Windows 98부터 깔끔하게 Visual C++ 5 sp3으로 빌드되기 시작했다. 그래서 얘들은 예외적으로 Rich 헤더가 포함되기 시작했다.

이런 9x와 달리 Windows NT는 운영체제 차원에서 처음부터 Visual C++ 팀과 잘 연계하는 편이었다.
9x 계열은 98에 와서야 msvcrt와 mfc42같은 Visual C++ 출신의 배포용 DLL들이 최초로 정식 포함돼 들어간 반면, NT 계열은 처음부터 메모장도 진작부터 msvcrt를 사용해 왔다. NT4는 직접 확인해 보지 않아서 모르겠지만, 2000은 모든 EXE/DLL의 내부에 Rich 헤더가 존재한다.

한편, Visual C++ 말고 델파이 같은 타 개발툴로 빌드된 실행 파일에는 Rich 헤더 같은 건 당연히 존재하지 않는다.

사용자 삽입 이미지

그럼 본론으로 들어가도록 하겠다.
마소에서는 이 헤더? 데이터? chunk?를 왜 집어넣기 시작했으며, 이 정보의 의미는 도대체 무엇일까? 엄밀히 말하면 헤더라고도 볼 수 없는 단순 데이터일 뿐인데 말이다.
놀랍게도 마소에서는 이에 대해서 지금까지 공식적인 답변을 한 번도 제공하지 않았으며 undocumented, 묵묵부답으로 일관한 듯하다. 빌드 시에 이 헤더를 제외시키는 옵션도 없다.

그래서 일각에서는 흉흉한 음모론까지 나돌기 시작했다. 제일 유명한 게 뭐냐 하면, 이건 이 바이너리를 빌드한 컴퓨터 환경을 식별하는 정보라는 것이다.
그래서 이 exe/dll이 악성 코드로 밝혀져서 경찰에 수사를 의뢰하게 되면.. 이 정보로부터 개발자의 컴퓨터를 추적할 수 있고, 따라서 악성 코드를 만든 사람도 아무 단서가 없는 것보다는 더 용이하게 색출할 수 있다고 한다..;;

이거 마치 컬러 복사기 얘기처럼 들린다. 컬러 복사기의 결과물에는 아주 정교한 워터마크가 사람 눈에 안 띄게 몰래 새겨진댄다. 그래서 어설픈 컬러 복사 위조지폐가 발견되면 그 워터마크를 토대로 복사기의 일련번호를 추적할 수 있으며, 이를 통해 범인도 색출할 수 있다고 한다. 허나 본인은 그런 게 실제로 존재한다고 믿지는 않는다.

뭐, 복사기는 그렇다 치고.. 실행 파일의 경우, 상식적으로 생각해 봐도 저건 개발 컴퓨터의 색출 목적으로 사용하기에는 보안이 너무 허술하다.
진짜 나쁜 마음 품은 악성 코드 개발자라면.. 그 Rich 헤더 부분을 후처리로 몽땅 0으로 칠해서 일부러 지워 버리기만 해도 자기 정체를 숨길 수 있으며, 그래도 악성 코드의 동작에는 하등 문제될 것 없다.

이 경우 PE 헤더의 다른 필드에 존재하는 checksum 정보가 어긋나서 파일이 변조되었다는 것이 감지되겠지만, 이것도 일부러 기재하지 않았다고 0을 집어넣어 버리면 그만이다. 더구나 보안을 위해 소프트웨어에서 이미 있던 이스터 에그도 다 없앴고 요즘은 바이너리 차원에서 철저히 예측 가능한 reproducible build까지 추구하는 마당에, 이런 식의 비밀 식별 정보는 마소의 개발 방침 이념과도 어울리지 않는다.

그러니 프로그램을 빌드할 때마다 내 컴퓨터의 맥 어드레스가 유출된다는 식으로 불안해할 필요는 없다. 하지만 Rich 헤더의 정체는 여전히 베일에 싸여 있었다.
그래서 전세계의 많은 컴덕과 해커들이 의문을 품기 시작했으며, 어떤 용자는 MS에서 개발한 링커 프로그램을 아예 근성으로 리버스 엔지니어링까지 했다. 그래서 이 데이터에 대한 여러 사실들을 밝혀냈다.

가장 먼저.. Rich 헤더는 4바이트 덩어리 단위로

A B B B X1 Y1 X2 Y2 ... "Rich" B

대체로 요런 형태로 돼 있다.
끝의 Rich 다음에 나오는 마지막 double word인 B가 일종의 난수이며, 암호화 key이다. 그리고 Rich 앞에 있는 숫자들은 바로 그 B와 xor을 해 주면 실제값을 얻을 수 있다.

그러면 맨 첫째 값 A는 언제나 0x44 0x61 0x6E 0x53.. 문자 형태로 늘어놓으면 "DanS"라는 시그니처가 된다.
다음으로 시그니처 뒤에 이어지는 몇 개의 B는 16바이트 단위 padding을 맞추기 위한 0값인 것 같다. 자기 자신을 xor 하면 결과는 언제나 0이 되니 말이다.

그 뒤 이어지는 같은 값들은 숫자 2개가 X, Y 형태로 한 pair를 이룬다. X는 이 바이너리를 생성하는 데 쓰인 툴(C 컴파일러, C++ 컴파일러, 리소스 컴파일러, 어셈블러 등..)과 버전(빌드 번호)을 나타내며, Y는 그 도구를 이용하여 생성된 아이템.. 이를테면 obj 파일의 개수를 나타낸다고 한다.

그럼 이런 정보들은 링커가 어디에서 얻어서 집어넣는가 하면.. 당연히 자기가 input으로 받아들이는 obj 파일로부터이다. 그렇잖아도 1997~98년을 전후해서 obj 파일 포맷이 바뀌었다고 한다.
obj야 소스 코드를 번역한 기계어 코드의 뭉치일 뿐이니 20년 전이나 지금이나 컴퓨터 아키텍처가 변함없는 한 바뀔 일이 없으며, 내부 구조가 바뀌어 봤자 새 버전의 컴파일러/링커가 인식하는 새로운 chunk 정도나 추가되는 게 전부일 것 같은데.. 꼭 그렇지는 않은 모양이다.

하긴, 같은 양의 코드를 빌드해도 요즘 컴파일러는 예전에 비해 obj의 파일 크기가 점점 더 커지고 있어 보이긴 하다. 디버깅 또는 최적화와 관련된 온갖 힌트와 메타정보들이 첨가되어서 그런 것 같다.
그래도 obj는 소스 코드를 빌드할 때마다 새로 생성되는 일회용 임시 파일일 뿐이니, 포맷에 하위 호환성 걱정 따위는 할 필요가 없을 것이다. 날려 봤자 빌드만 다시 하면 될 일이고..

아무튼 흥미로운 사실을 알게 됐다.
개인 정보 유출은 아니고, 그렇다고 디버깅과 관련된 힌트도 아니고 저런 정보가 마소의 개발툴에서 도대체 왜 들어갔는지는 여전히 오리무중이다만.. 그래도 내역을 까맣게 모르던 시절보다는 상황이 나아졌다.
본인이 이 글을 쓰기 위해 검색하고 참고한 외국 사이트는 다음과 같으니,  더 자세한 관심이 있는 분은 참고하시기 바란다.

The devil’s in the Rich header
Microsoft's Rich Signature (undocumented)
The Undocumented Microsoft "Rich" Header
Article: Things They Didn't Tell You About MS LINK and the PE Header

저 외국 사이트들의 설명에 따르면, Rich와 DanS는 모두 1990년대에 마소의 Visual C++ 팀에서 근무하고 이 데이터의 구현에 직접 관여했던 프로그래머의 이름에서 유래되었을 가능성이 높다고 한다. 그 이름은 각각 Richard Shupak와 Dan Spalding이다. ㄷㄷㄷ;;
MZ, PK만큼이나 프로그래머의 이름이 파일 포맷에 각인된 사례라 하겠다.

참고로 전자 리처드의 경우, Windows SDK에서 psapi.h의 작성자로 이름이 나와 있기도 하다.
PSAPI는 Windows NT 계열용으로 실행 중인 프로세스 정보를 조회하는 EnumProcesses, GetModuleFileNameEx 등의 함수를 정의하는 라이브러리인데, 작성 날짜가 무려 1994년이라고 적혀 있다.

Posted by 사무엘

2019/05/21 08:31 2019/05/21 08:31
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1621

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

본인은 박사 과정에 진학해서 수업을 다 듣고 종합 시험도 통과한 뒤, 지난 2년이 넘는 기간 동안 휴학을 했다. 그리고 그 기간 동안 날개셋 한글 입력기는 8.6에서 9.7까지 올라갔다.
입력기 연구의 연장선에다가 글꼴 연구도 새로 접목하는 것을 목표로 진학했었는데, 입력기 연구가 당초 계획보다 다소 오래 걸렸다.

이제 입력기는 파일 포맷과 엔진 구조를 다 뜯어고칠 정도로 너무 비현실적인 추상화(재설계나 리팩터링), 아니면 너무 이상적인 수준의 기능을 제외하고 어지간히 규칙 기반 한글 입력과 관련된 것들은 다 통달했으며 잘 실현됐다. 9.7도 특별히 심각한 문제 없이 아주 잘 만들어졌다.

다만, 날개셋은 TSF 기반의 한글 IME일 뿐만 아니라 반대로 타 IME들을 구동해 주는 텍스트 에디터이기도 하니.. 요즘은 편집기에서 타 IME를 구동하는 동작과 관련된 이슈들을 좀 살펴보고 있다.

1. 내 프로그램에서 9.7 이후로 개선된 사항

(1) 외부 모듈의 옛한글 조합을 여느 블록(selection)과 달리 취급

날개셋 편집기에서 입력 항목을 '빈 입력 스키마'로 고르면 앞서 언급한 바와 같이 자체 입력기가 아니라 다른 외부 IME들을 사용할 수 있다.
그런데 한글 IME로 현대 한글을 조합할 때는 깜빡이는 네모 cursor가 나타나는 반면, 옛한글을 조합할 때는 조합이 그냥 블록 형태로 잡힌다. 그래서 자체 입력기로 옛한글을 입력할 때와는 달리 이질적이고 아마추어스러운 느낌이 난다.

이건 일차적으로는 운영체제에서 옛한글처럼 내부적으로 2개 이상의 코드값으로 표시되는 한글에 대한 배려를 안 해서 그렇다. 조합 문자열이 한글로만 이뤄져 있을 때 응용 프로그램이 강제로 보정을 해서 깜빡이는 네모 cursor를 구현하라면 할 수도 있다.

내 프로그램에서는 그렇게까지는 안 하는 대신, 비록 블록처럼 보이더라도 진짜 블록이 잡힌 것처럼 복사/잘라내기 버튼이 켜지지도 않게 프로그램의 동작을 깨알같이 개선했다. 그건 블록이 아니라 조합을 표시하는 용도일 뿐이기 때문이다..

사용자 삽입 이미지

(2) 붙여넣기를 할 때 외부 모듈의 조합이 덧나지 않게

또한, 자체 입력기가 아닌 외부 IME로 한글을 조합하고 있던 중에 도구모음줄의 '붙여넣기' 버튼을 마우스로 누르면..
자체 입력기를 사용할 때와 마찬가지로 조합이 종료된 뒤에 클립보드 내용이 삽입되는게 정상이다.

하지만 지금까지는 그렇지 않았다. 조합이 중단되고 그 문자열이 사라지고서 클립보드 내용이 삽입되었다. 이 버그를 발견하여 고쳤다.

사용자 삽입 이미지

이 두 가지 사항은 언제쯤 다음 버전에 반영되어 나올지 모르겠다.

2. 내 프로그램과 무관한 운영체제의 버그

(1) IME 도구모음줄이 두 종류 모두 표시됨

Windows 10 1803 버전 기준으로..
IME의 구형 재래식 도구모음줄과 Windows 8 스타일의 간소화 도구모음줄이 다같이 동시에 뜨는 경우가 있다. 정확한 재연 조건은 잘 모르겠지만 컴퓨터를 절전 상태로 껐다가 다시 켰을 때 가끔, 그러나 확실하게 이런 현상이 발생한다.

사용자 삽입 이미지

"고급 키보드 설정"에서 "사용 가능한 경우 바탕 화면 입력 도구 모음 사용" 옵션을 건드려 주면 다시 둘 중 하나만 나타나게 개선되긴 한다. 하지만 이건 운영체제의 버그이니 나중에 업데이트를 통해 해결되어야 할 것이다. Windows 8은 물론이고 10도 초창기에는 이런 현상이 발생한 적이 없었다.

(2) 일본어 IME의 조합 관리 버그

날개셋 편집기 또는 IE/Edge 브라우저의 텍스트 입력 폼에서 Microsoft 일본어 IME를 구동하고, 히라가나 모드에서 일본어를 몇 자 입력한다.
space를 눌러서 그 일본어 문자를 변환은 하지 말고, 좌우 화살표 키를 눌러서 조합 영역을 빠져나간다. 그러면 조합을 나타내는 밑줄이 일시적으로 사라진다.

그 뒤에 caret이 기존 조합 영역으로 돌아오면 기존 조합이 다시 생겨야 되는데 그리 되지 않는다.
그 상태에서 다른 곳에서 Shift+화살표를 눌러서 블록을 만들어 보면 아까 조합하던 일본어 문자가 덧나서 잘못 삽입된다.

사용자 삽입 이미지

이 버그는 Windows 10의 16xx대 이전 버전에서는 발생하지 않다가 후대 버전에서 나타났다. 1803의 후대 버전에서는 어찌 되었나 모르겠다. 날개셋 편집기뿐만 아니라 MS에서 만든 TSF A급 웹브라우저에서 모두 동일하게 발생하니 내 프로그램만의 문제도 아니다.

단, 워드패드에서는 동일 운영체제와 동일 IME에서 저런 오동작이 발생하지 않는다. 서식을 지원하기도 하니 에디팅 엔진 차원에서 무슨 차이가 있어서 그런 것 같다.

(3) 옛한글 IME의 조합 영역 처리 버그

이건 Windows 8 이래로 계속 동일한 것 같은데..
마소에서 제공하는 옛한글 입력기는 초성이나 중성에 옛한글이 들어간 상태에서 종성의 첫 타를 입력하면.. caret 위치가 좀 이상하게 찍힌다. 내부적으로 표현되는 글자 수가 3자가 되었으니 0~3까지 모두 조합 영역으로 설정해야 하는데 종성이 입력되기 전처럼 0~2까지만 설정한다.
그래서 날개셋 편집기에서는 화면이 일시적으로 이렇게 표시된다. 종성 둘째 타 이후부터는 다시 괜찮아진다.

사용자 삽입 이미지

시각적으로 좀 이상한 것 말고 다른 오동작은 없다. 하지만 날개셋, 한컴 입력기 등 옛한글 입력을 지원하는 다른 모든  IME에는 이런 현상이 없고 MS IME만 저러니.. 이건 저 프로그램만이 단독으로 해결해야 할 문제로 보인다.

3. 단순 차이점 -- 옛한글 filler 글쇠

두벌식 옛한글 글자판에는 중성이 빠진 미완성 한글 내지 종성 단독 낱자를 입력하기 위해서 일명 filler라는 글쇠가 있다. 위치는 관례적으로 Shift+J로, 날개셋, 아래아한글, MS 옛한글 입력기가 모두 동일하다.

날개셋과 아래아한글에서는 이 filler라는 게 언제나 '중성 filler'를 의미한다. 이것만 있어도 초성이나 종성이 없는 글자는 입력 가능하기 때문이다.
하지만 MS의 경우, filler도 뭔가 두벌식스럽게 글자를 완전 처음 입력할 때는 빈 자리에다가 '초성 filler'를 흉내 내어 주는 것 같다. 굳이 그럴 필요가 없지만 말이다.

그래서 초기 상태에서 종성을 단독으로 입력하려면 filler를 한 번이 아닌 두 번 눌러야 한다. 본인은 처음엔 이런 차이를 몰라서 마소 옛한글 입력기로는 종성 단독 입력이 불가능한 줄 알았다.
초성 filler도 지원해 주는 게 사람에 따라서는 더 직관적으로 보일 수도 있다. 하지만 한글을 연속으로 입력하기 시작하면 filler는 어차피 사실상 중성으로만 동작해야 하기 때문에 굳이 저럴 예외를 둘 필요가 있나 싶다.

중요한 건 이런 동작조차도 표준으로 딱 정해진 게 없어서 프로그램마다 차이가 있을 수 있다는 점이다. 날개셋에서는 글쇠배열의 수식을 바꿔 주면 지금 동작(중성 고정)뿐만 아니라 MS IME의 동작도 물론 구현할 수 있다.

4. 원인을 알 수 없는 문제

다음은 본인의 개발 환경에서 아주 드물게 발생하는 것을 확인하긴 했지만 재연 조건을 전혀 몰라서 좀 난감한 지경에 있는 버그 아이템들이다. 이것들이 문제의 원인이 전적으로 내 프로그램의 귀책사유로 판명되어 해결된다면.. 위의 1번의 개선 사항까지 포함해서 다음 버전인 9.71이 지금이라도 당장 나오게 된다.

(1) 여전히 발생하는 랙

이번 9.7에서는 안 그래도 편집기의 에디팅 엔진과 관련된 몇몇 버그들이 잡히고 내부 동작 방식이 최적화 됐다. 그런데 편집기를 한번 띄워 놓고 며칠 이상 오래--특히 중간에 컴의 절전 모드와 복귀를 수차례 반복할 정도로-- 쓰다 보면, 어느 샌가 글자가 화면에 나타나는 속도가 내 타자 속도를 못 따라갈 정도로 랙이 걸리는 경우가 여전히 발생한다.

게다가 이게 참 악랄한 게.. 랙의 발생하던 당시에 발생 조건이 다음과 같이 가변적이었다는 것이다.

  • 오로지 날개셋 외부 모듈로 한글을 입력할 때만 느려짐 (MS IME, 자체 입력기 등등은 괜찮음)
  • 외부 모듈로 한글을 입력할 때만 느려짐 (날개셋, MS IME에서 랙. 자체 입력기는 괜찮음)
  • 아무 방식으로나 글자를 입력할 때 몽땅 느려짐

마지막으로 이 문제가 발생했을 때엔.. 처음엔 외부 모듈에서만 발생하는 것 같더니 이내 상황이 최악으로 바뀌었다.
특정 문서의 맨 마지막 줄에서 글자를 입력할 때만 극심한 랙이 걸리고, 그렇지 않을 때는 괜찮았다(타 문서 or 다른 줄). 심지어 그 문서에서 편집하고 있던 텍스트를 몽땅 지우고 새로 입력을 시작해도 랙이 사라지지 않았다.

이 랙이 발생하는 동안 내 프로그램의 내부에서는 무슨 일이 벌어지고 있고 도대체 어느 계층에서 뺑뺑이를 도는 건지 도무지 알 길이 없다. 그냥 평범하게 프로그램을 띄워서는 절대로 발생하지 않는다. 그나마 유력한 단서가 될 만한 현상은.. 이때 날개셋 편집기가 다음과 같이 memory leak이 발생해 있었다는 것이다.

(2) MS IME를 사용할 때 발생하는 괴이한 memory leak

날개셋 편집기와 작업 관리자를 같이 실행한다. 다음으로, 편집기에서 TSF 지원 옵션을 켠 상태에서 '빈 입력 스키마'를 고른다.
Microsoft 기본 한글 IME로, "세벌식 390/최종"(두벌식 말고)으로 "ㅇ.ㅇ.ㅇ.ㅇ." 처럼.. 한글 + 비한글 문자를 수십 회 쭈룩쭈룩 교대로 입력해 보라.

그러면 초기에 2~3MB대 안팎이던 프로세스 메모리 사용량이 계속해서 증가하는 게 관찰된다. 명백하게 memory leak이다.
COM 오브젝트 간의 reference count 같은 게 꼬인 것 같은데.. 이건 도대체 누구 잘못이라고 봐야 할까?

당연히, 디버그 빌드에서 단순 memory leak detector로는 문제가 전혀 감지되지 않는다. 내 프로그램은 10수 년에 달하는 짬밥을 자랑하며 얼마나 오랫동안 안정화가 돼 왔는데.. 소스 코드 상으로 무식한 결함이 있지는 않다.

더구나 날개셋, 한컴 입력기 등 "타 IME에서는 이런 현상이 없다." MS IME도 세벌식을 쓰고 있을 때만 저렇고 두벌식일 때는 문제 없다.
그리고 Windows Vista, 7, 10에서 이 현상을 확인했다. 구닥다리 XP에서는 MS IME+세벌식에서도 문제가 없다.

그럼 내 과실 0, 마소 과실 100을 입증하려면 날개셋 편집기 말고 다른 프로그램에서도 MS IME + 세벌식으로 저렇게 쳤을 때 동일한 memory leak이 발생한다는 걸 입증해야 하는데 그건 또 그렇지 않아 보인다~! 워드패드, MS Word, IE, Edge 등 TSF를 지원하는 프로그램들은 또 희한하게도 저런 현상이 발생하지 않는다.

다음으로 지푸라기를 잡는 심정으로 날개셋 구버전까지도 구해서 써 봤다.
날개셋 8.0까지는 이 문제가 없고, 8.4부터 leak이 발생하더라. 8.2는 불명.. 그러니 이 버그는 대략 2016년부터 있어 왔다는 것이다.
이때 도대체 어떤 변화가 있었는지.. 본인은 프로그램 소스를 자주 백업하는 편이지만, 컴퓨터가 바뀌는 과정에서 지금으로부터 3년이 넘게 너무 오래된 소스는 갖고 있지 않아서 이 방법으로도 문제의 원인을 파악할 수 없었다. ㅠㅠ

난 Windows용 IME라는 물건을 개발하느라 지난 10여 년 동안 온갖 희한한 버그 신고들을 받고 기상천외한 지저분한 환경에서 디버깅을 했다. 그러면서 마치 교통사고 과실 비율 따지는 것 같은 현상들을 많이 경험했었다.
둘 다 스펙대로 100% 무결하게 구현된 건 아니었고(혹은 스펙 자체가 모호해서..) 둘 다 조금만 조심하면 됐는데 둘 다 무데뽀로 동작해서 운 나쁘게 문제가 발생하는 것.. 말이다. Windows용 IME라는 바닥은 무척 "구리다."

아무튼 현재로서는 저 memory leak의 원인과 해결 방법이 오리무중이다. 해결만 된다면 9.7 다음으로 9.71이 당장 나와야 할 것이다.

(3) 제어판 닫았을 때 프로그램 뻗나?

정말 민망하고 황당한 버그인데.. 올해 들어 두세 번인가 겪었다.
날개셋 편집기에서 제어판을 꺼내서 설정을 바꾼 뒤, 확인을 눌러서 닫았더니 편집기 프로그램이 그냥 대짜로 뻗어 버렸다.
한 번 발생했을 때는 그냥 재수없는 우연인가 싶었는데, 몇 주 전에 마지막으로 동일 문제를 겪었을 때는 '미저장 확인'을 누른 것만으로도 뻗었다.

물론 그 뒤로는 편집기에서 날개셋 외부 모듈을 또 얹고, 제어판에서 온갖 설정을 바꾸고 빠른설정을 띄우면서 지지고 볶아 봐도.. 동일 문제가 다시는 재발하지 않고 있다. 위의 랙도 몹시 드물게 발생하지만 이 crash는 그것보다 더 드물게 발생했다. 그래서 난감하다.

Posted by 사무엘

2019/03/31 08:30 2019/03/31 08:30
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1603

* 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

오늘은 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

Windows API에서 LoadCursor는 EXE/DLL 실행 파일의 리소스로부터 마우스 포인터를 얻어 오는 함수이다. 아니면 모듈 핸들 값을 NULL로 생략하면, 시스템이 제공하는 다양한 공용 포인터를 얻을 수도 있다. 일반적인 화살표 아니면 모래시계, 텍스트 입력란용 I-beam 등등 말이다.

그런 known 포인터의 명칭은 IDC_ARROW, IDC_IBEAM, IDC_WAIT ... 등으로 10여 종이 WinUser.h에 정의돼 있다. 실제값은 그냥 32xxx대의 리소스 ID 정수이다.

그런데, 제어판의 마우스 포인터 설정에 나열되어 있는 공통 포인터 중, 유일하게 IDC_* 명칭이 전혀 부여되지 않은 포인터가 하나 있다. 바로 펜 모양의 필기 포인터이다.
MSDN 문서와 WinUser.h를 눈을 씻고 찾아 보시라. 무려 Windows 95 이래로 제어판에 버젓이 등재되어 온 표준 공통 포인터임에도 불구하고 이름이 없다. 신기하지 않은가?

사용자 삽입 이미지

사실, 이 펜이랑 xor 반전 십자가인 IDC_CROSS(정밀도 선택), 그리고 IDC_UPARROW(대체 선택)는 응용 프로그램에서 거의 볼 일이 없긴 했다. =_=;;

그래서 본인은 장난기가 발동했다.
1부터 65535까지 brute-force로 LoadCursor 요청을 해서 문서화되지 않은 마우스 포인터가 돌아오는 게 있는지 역대 Windows 운영체제별로 확인을 해 봤다.

사용자 삽입 이미지

결과는 꽤 흥미로웠다.
답부터 말하자면 펜 모양은 32631이라는 ID가 홀로 부여되어 있었다. Windows 95부터 10까지 동일하게 사용 가능하다.
'홀로'라는 말은 인접한 32630이나 32632 같은 숫자에는 포인터가 배당된 게 없다는 뜻이다.

모든 Winows에는 100부터 11x번에 완전 기본 마우스 포인터가 할당되어 있었다. 즉, Aero 포인터를 쓰고 있더라도 여기에는 완전 운영체제 기본 흑백 화살표 포인터들이 있으며, 얘들은 포인터 뒤에 입체감을 주는 그림자도 표시되지 않았다. 이건 무슨 다른 특수한 용도로 쓰이는가 보다.

그리고 IDC_HELP 다음으로 32652부터 32662 사이에 있는 11개의 포인터는.. 놀랍게도 마우스 휠을 눌러서 자동 스크롤 모드가 됐을 때 나타나는 '작은 원 + 검은 삼각형'들이었다(각 방향별로). 그것도 휠이 운영체제 차원에서 정식 지원되기 시작한 Windows 98부터 20년째 동일한 형태로 존재하고 있었다. 이건 기술적으로는 user32.dll에 존재하는 리소스이다.

그런데 이런 걸 도대체 왜 문서화하지 않았을까? Windows 98부터는 하이퍼링크용 IDC_HAND만 추가됐다고 달랑 써 놓고 입 싹 씻은 걸까..? 뭔가 단단히 속은 느낌이었다.

본인은 당장 날개셋 한글 입력기에다가 조치를 취했다.
날개셋 한글 입력기는 16년 전(2002...)에 나온 2.0 이래로 지금까지 자동 스크롤 모드용 마우스 포인터들을 내장하고 있었다. 그걸 모두 제거하고, (1) 운영체제가 비공식적으로 제공하는 이 포인터를 사용하게 했다. 그래서 파일 크기가 4~5KB 남짓 감소하는 효과를 얻었다.

(2) 그리고 최근에 추가된 필기 인식 입력 도구에서 마우스를 그리기 입력란 내부로 가져가면 포인터가 펜 모양으로 바뀌게 했다. 뭔가를 그리면 된다는 것을 강조하기 위해서이다.
결과물을 보니 만족스럽다. 이 달 초에 나온 9.61 버전에 바로 요 사항들이 반영되었다.

사용자 삽입 이미지

이것 말고 문서화되지 않은 포인터로는 32663이 있는데, 일반 화살표 포인터 옆에 모래시계 대신 의외로 CD 아이콘이 자그맣게 붙어 있다.
광학 드라이브가 백그라운드에서 뭔가 돌아가고 있을 때 표시되는 듯하며 본인도 이걸 본 기억은 있다. 하지만 정확한 표시 조건은 잘 모르겠다.

차라리 화살표 옆에 점선 사각형 내지 [+]가 붙어서 drag & drop을 나타내는 포인터가 더 자주 쓰이며, 공통 포인터로 등재됐으면 좋겠는데 얘들은 그렇지 못하다. 그냥 ole32.dll에 하드코딩된 리소스가 쓰인다. 그리고 창 전체의 크기 말고 창 내부의 splitter 구획의 폭을 조절할 때 바뀌는 포인터도 창 크기 조절용과는 다른 걸 쓰는 게 UI 디자인상으로 맞는데 그것들 역시 공통 포인터에는 없다. 그렇기 때문에 여전히 싸제 자체 내장에 의존하거나, 아니면 comctl32.dll에 하드코딩된 리소스를 슬쩍 가져오는 게 통용된다.

아무튼, 오늘은 마우스 포인터와 관련하여 새로운 사실을 알게 됐다.
그러고 보니 옛날에 16비트 시절에는 메모리 공간이 엄청나게 부족하기도 하고, GDI 핸들의 번호 영역 자체가 몇 만 남짓밖에 안 되었다. 그러니 Windows 3.1뿐만 아니라 9x에서도.. 아까 본인이 했던 것처럼 1부터 65535까지 brute-force 식으로 대입해서 시스템에 현재 존재하는 비트맵· 아이콘 따위를 몽땅 나열하고 조회하는 툴을 만드는 것도 가능했다.

오늘날 32/64비트 시대에도 DLL의 심벌 ordinal 번호와 리소스 ID 번호는 16비트 영역으로 한정돼 있다. 이 둘에서는 숫자와 문자열이 식별 용도로 모두 쓰이며, 16비트를 초과하는 큰 숫자는 문자열 포인터인 것으로 간주되게 의미가 예약돼 있기 때문이다.

Posted by 사무엘

2018/12/21 08:33 2018/12/21 08:33
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1567

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 : ... 18 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/09   »
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          

Site Stats

Total hits:
1252676
Today:
339
Yesterday:
441