1. 멀티스레드

지난 날개셋 한글 입력기 9.9에서는 어쩌다 보니 스레드와 관련된 작업이 좀 있었다. 각종 대화상자에서 스레드 작업이 행해지던 것을 중단시켰을 때 프로그램이 멎던 문제를 해결했으며, 반대로 사용자의 타이핑에 실시간으로 반응하며 동작하는 일부 입력 도구에다가는 랙 없이 깔끔하게 동작하기 위해 스레드를 사용하게 하는 옵션을 추가했다. 뭐, 요즘 컴퓨터에서 랙을 걱정할 지경은 아니긴 하지만 말이다.

오랜만에 멀티스레드를 다루다 보니 지금까지 이론으로만 알고 있던 동기화라는 것도 구현해야 했다. worker 스레드는 입력 데이터를 지지고 볶고 열심히 건드리고 있는데, 그걸 표시하는 UI 스레드도 아무 통제 없이 같이 돌아가게 놔 두면 프로그램은 당연히 메모리 에러가 나고 뻗는다.

Critical section을 생성하고 소멸하는 부분은 클래스의 생성자와 소멸자에다 담당시켰는데, 이걸 이용해서 실제로 락을 걸고 푸는 것까지 또 다른 클래스의 생성자와 소멸자로.. 그렇다고 기존 오브젝트의 포인터를 명시적으로 지정하지 않고 자동 구현하는 것은 C++에서 안 되는가 보다. Inner class에서 바깥 class의 non-static 멤버에 접근 가능하지 않으니까..

임계 구간을 잘 설정해야 crash를 막을 수 있다. 하지만 그렇다고 개나 소나 마구 락을 걸어 버리면 성능만 떨어지는 게 아니라 반대로 deadlock이라는 부작용이 발생해서 프로그램의 응답이 멎어 버릴 수 있다. Crash는 사람이 다쳐서 의식을 잃는 것이고, deadlock은 사람이 수술 후 마취에서 못 깨어나는 것과 비슷하다..;;

주로 어떤 상황이냐 하면, UI 스레드가 worker 스레드의 실행이 끝날 때까지 기다리게 됐는데 worker 스레드는 데이터를 고친 뒤 UI 스레드의 응답이 필요한 요청을 해 버릴 때 이렇게 된다. 특히 윈도우에다가 메시지를 보내고 응답을 기다리는 API 함수를 호출하는 것 말이다.

이런 주의 사항을 염두에 두면서 프로그램의 주요 구간에 스레드 동기화를 시켜 놓았다.
당연한 말이지만 한 critical section 오브젝트는 단일 구간이 아니라 코드의 여러 구간에다가도 배치해서 이들 모든 구간을 통틀어서 한 순간엔 한 스레드만이 접근 가능하게 제약을 걸 수 있다. 이게 강력한 면모인 것 같다.

그러고 보니 Windows 프로그래밍 헤더에 들어있는 구조체들 중에는 내부 구조가 딱히 공개돼 있지 않은 것이 좀 있다.
가령, PAINTSTRUCT 내부에 있는 rgbReserved 같은 멤버, STARTUPINFO 내부에 있는 cbReserved2, 그리고 CRITICAL_SECTION도 어쩌면 CPU 아키텍처별로 크기와 내부 멤버가 제각각 다를 가능성이 높은 비공개 구조체이다.
이런 예가 무엇이 더 있을 텐데 궁금하다. 당장은 떠오르지 않는다.

2. C 라이브러리와 스레드 생성 함수

Windows에서 스레드를 생성하는 제일 저수준 API는 잘 알다시피 CreateThread 함수이다.
요즘이야 C++도 C++11부터 언어 라이브러리 차원에서 플랫폼· 운영체제를 불문하고 멀티스레드 기능을 std::thread, std::mutex 같은 클래스로 제공하고 있지만, 이들도 내부적으로는 물론 Windows API 같은 운영체제 API를 호출하는 형태로 구현된다. 심지어 native handle값을 되돌리는 멤버 함수도 제공한다.

(그러고 보니 C++ 라이브러리는 C 라이브러리와 달리 라이브러리의 소스가 공개돼 있지 않은 듯하다.. 소스가 어쩔 수 없이 공개되는 템플릿 쪽은 도저히 알아볼 수 없는 난독화 급으로 작성돼 있고, 내부적으로 호출하는 함수 같은 것은 구현체 소스가 딱히 보이지 않는다.)

C++11 이전에는 Visual C++의 경우, 언어 확장 명목으로 CreateThread (와 ExitThread)를 얇게 감싼 _beginthread[ex] (+ _endthread[ex]) 함수를 독자적으로 제공해 왔다.
얘는 Windows SDK의 헤더와 라이브러리를 끌어들이지 않고도 스레드를 사용할 수 있게 해 주는 동시에.. C 라이브러리가 자체적으로 스레드별로 사용하는 메모리를 제때 할당하고 제때 해제하는 역할도 담당했다.
그렇기 때문에 C/C++ 함수도 사용하면서 Windows용 프로그램을 개발한다면 일반적으로는 운영체제 직통 함수 대신 이런 상위 함수를 사용해서 스레드를 생성해야 한다.

C 라이브러리가 스레드별로 무슨 메모리를 사용하는가 하면.. 전역변수로 단순하게 구현되었지만 지금 관점에서 보면 스레드별로 값이 제각기 보존되어야 하는 정보들 말이다. errno라든가, strtok 함수의 내부 state 및 context.. 비록 이런 것에 의존하는 함수가 그리 많은 건 아니지만 어쨌든 그런 예가 있다.

C 라이브러리에서 제공하는 begin/end 함수는 ex가 붙은 놈과 그렇지 않은 단순한 놈으로 나뉜다. ex는 받아들이는 인자가 Windows API의 그것과 완전히 동일하다. 그에 비해 단순한 놈은 평소에 잘 쓰이지 않는 인자들을 과감히 생략했기 때문에 사용하기가 더 쉽다.

예를 들어 begin 버전은 SECURITY_ATTRIBUTES 정보를 입력받는 부분이 생략됐고, 거의까지는 아니어도 잘 쓰이지 않는 thread ID를 받아들이는 부분도 생략됐다. 또한, 프로세스도 아니고 스레드의 실행 후 리턴값은 거의 쓰이지 않는다는 점을 감안하여 end 버전은 리턴값을 지정하는 부분도 생략되고 그냥 0으로 고정됐다.

참고로 Windows NT의 경우 ReadFile/WriteFile에서 실제로 읽거나 쓰는 데 성공한 양을 받는 DWORD 포인터, 그리고 CreateThread에서 생성된 스레드의 ID를 받는 DWORD 포인터를 생략하고 NULL을 줄 수 있다. 굳이 그 정보들이 필요하지 않다면 말이다. 하지만 과거의 Windows 9x는 저들 함수의 인자에서 NULL을 줄 수 없으며, 그냥 잉여 변수라도 하나 반드시 선언해서 넘겨줘야 한다는 차이가 있었다(NULL 주면 실행 실패).

뭐 그렇긴 한데.. C 라이브러리의 간편 버전은 사용법을 간소화하기 위해 여러 인자들을 생략했을 뿐만 아니라, 스레드 핸들을 CloseHandle로 닫는 것까지 임의로 자동으로 해 버린다. 스레드가 아주 잠깐 동안만 실행됐다가 종료돼 버리는 경우, _beginthread 함수의 리턴값이 나중의 시점에서 유효하다는 보장을 할 수 없다.

이건 사족에 가까운 간소화 조치로 여겨지며, 이 때문에 _beginthread는 사용이 그리 권장되지 않고 있다. 아니면 정말 뒷감당 할 필요 없고 한번 생성된 뒤에 “나는 책임 안 지니 니가 알아서 모든 뒷정리 하고 곱게 사라져라” 급인 스레드를 간편하게 생성하는 용도로나 적합하다. 그게 아니면 번거롭지만 결국은 _ex 함수를 쓰고 핸들은 내가 수동으로 닫아 줘야 한다.

3. 실행 주체들의 종료 리턴값

프로세스와 스레드는 종료될 때 정수를 하나 되돌릴 수 있는데, 아무 일 없이 잘 끝났으면 간단히 0을 되돌리는 게 관행이다. 옛날에 도스에서는 프로세스의 종료 코드를 ERROR_LEVEL이라는 독특한 명칭으로 불렀었다.

프로세스의 종료값은 그럭저럭 고유한 용도가 있으며, 여러 프로그램들을 순차적으로 실행하는 배치 파일이나 스크립트 같은 데서 많이 쓰인다. 종료되고 없는 먼젓번 프로그램의 실행이 성공했는지 여부를 밖에서 알 필요가 있기 때문이다.

하지만 앞에서 잠시 언급한 바와 같이.. 스레드의 종료값은 내 경험상 거의 쓰이지 않는다. 스레드의 종료값을 읽고 판단할 수 있는 코드 자체가 동일 프로세스 안의 다른 스레드밖에 없고.. 한 프로세스 안에서는 굳이 종료값 말고도 스레드의 실행 결과를 알 수 있는 방법이 매우 많기 때문이다. 애초에 같은 메모리 주소 안이니..

또한 스레드는 애초에 여러 작업을 동시에 수행하라고 만들어지지, 배치 파일처럼 여러 스레드를 순차적으로 실행하면서 실행 결과를 확인하는 식으로 운용되지도 않는다. 그럴 거면 그냥 한 스레드에서 순차 실행을 구현하고 말지.. 프로세스와는 여러 모로 사용 여건이 다르다.

그 와중에 Windows에서는 259라는 값이 STILL_ACTIVE라는 의미로 예약돼 있는지라.. 프로세스나 스레드의 종료 리턴값으로 쟤를 돌려주는 것이 금지되어 있다. 자기는 종료되어서 없는데 GetExitCodeProcess/Thread 함수가 저 값을 되돌리고 있으면 다른 프로세스나 스레드는.. 없는 프/스의 실행이 끝날 때까지 기다리면서 데드락 같은 상태에 빠지기 때문이다.

4. 타 프로세스에다 내 스레드 생성하기: CreateRemoteThread

Windows API 중에는 기본적인 기능을 제공하는 함수가 있고, 거기에 덧붙여 어느 프로세스 문맥에서 그 기능을 수행할 것인지를 지정하는 인자가 추가로 붙은 Ex버전이 또 존재하는 경우가 있다.
예를 들어 ExitProcess(현재 프로세스만)의 상위 호환인 TerminateProcess, 그리고 VirtualAlloc에다가 HANDLE hProcess가 추가된 VirtualAllocEx처럼 말이다.

그것처럼 스레드를 생성하는 CreateThread에 대해서도 target 프로세스를 지정할 수 있는 CreateRemoteThread 함수라는 게 있다.
이런 함수들은 내부적으로 구현도 하위 함수를 호출하면 상위 함수에다가 GetCurrentProcess()의 리턴값을 붙여서 호출하는 형태로 돼 있다.

그런데 남의 프로세스 주소 공간에다가 스레드를 생성한다니??
이건 물론 디버거나 보안 솔루션 같은 특수한 프로그램에서나 필요하지 자기 일만 하는 일반적인 프로그램에서 쓰일 일은 없는 기능이다.
그리고 이 함수는 각종 명령 인자들을 준비하는 게 몹시 까다롭기 때문에 타 프로세스에서는 매우 제한된 형태로밖에 활용을 할 수 없다.

제일 근본적으로는.. 실행되어야 할 스레드 함수 코드와 실행에 필요한 데이터들이 몽땅 그 target 프로세스의 메모리에 마련돼 있어야 하며 주소값 또한 걔네들의 문맥으로 줘야 하기 때문이다. 잘못되면? 요청을 한 우리가 아니라 멀쩡히 돌아가던 저 프로세스가 뻑나게 된다.
global hook 프로시저를 지정할 때처럼 DLL에다가만 분리해서 구현해 놓으면 그 DLL이 알아서 거기로 주입.. 그런 것도 없다.

다만, CreateRemoteThread를 이용해서 타 프로세스에다 내 코드를 주입하려면 어차피 DLL을 만들기는 해야 한다. 그 이유는 이렇다.
CreateRemoteThread로 할 수 있는 게 사실상.. 스레드 callback 함수로 LoadLibrary를 지정하는 것밖에 없기 때문이다.

이게 가능한 이유는 (1) LoadLibrary(+ FreeLibrary도)가 machine word 하나를 받아서 word 하나를 되돌린다는.. 스레드 callback 함수와 prototype이 일치하기 때문이며, 그리고 (2) LoadLibrary가 소속된 kernel32.dll은 어느 프로세스에서도 메모리에 load된 주소가 동일 고정이기 때문이다. 즉, LoadLibraryA/W 함수의 주소도 고정이며 예측 가능하다. 요게 핵심이다. 요즘 보안을 위한 대세인 ASLR에서 열외된 영역인 듯..

물론 LoadLibrary에다가 주는 인자는 평범한 숫자가 아니라 포인터이므로 넘겨줄 때 조심해야 한다.
VirtualAllocEx를 이용해서 그 프로세스 문맥에서의 메모리와 포인터 주소를 얻은 뒤, 문자열을 그쪽으로 써 넣을 때도 WriteProcessMemory를 호출해서 하면 된다. 얘는 그야말로 memcpy를 fwrite처럼 구현한 상위 호환 버전이라고 생각하면 되겠다.

LoadLibrary에다가 넘겨주는 문자열 인자로 바로 상대방의 프로세스에서 수행할 작업(뭔가 엿보기, 훅킹하기 등등)이 구현된 내 DLL의 주소를 넣으면 된다. DllMain 함수에서 돌아가는 것이니 몸 사리면서 조심해서 실행돼야 한다.
혹시 내 DLL을 실행하면서 다른 DLL을 읽어 놓아야 한다면 내 DLL을 주입한 것과 동일한 테크닉으로 그런 dependency DLL들을 미리 읽어 놓는 게 좋을 것이다. LoadLibrary를 수행하던 스레드가 실행이 끝났다고 해서 그 DLL들이 해제되는 건 아니기 때문이다.

주입되었던 내 DLL을 빼내는 것도.. 간단하다.
스레드 함수에다가 FreeLibrary를 주고, 인자로는 저 target 프로세스에서 되돌려진 내 DLL의 핸들값을 주면 된다.
target 프로세스에서 되돌려진 핸들값이야 내 DLL의 DllMain 함수의 인자로 들어왔을 테니 모를 수가 없을 테고, 그걸 호스트에다가 전하는 건 어려운 일이 아닐 것이다.

이런 식으로 저 함수에다가 LoadLibrary 대신 ExitProcess를 주면 그 프로세스가 알아서 자기 자신을 강제 종료하게 되니, 사실상 TerminateProcess와 같은 효과도 낼 수 있다.

16비트 시절에는 단일 address 공간에서 시스템 전체에 영향을 끼치는 프로그램을 만드는 게 용이했기 때문에 그에 상응하는 점프/복귀 주소 변조, x86 어셈블리어 삽입 같은 꼼수 테크닉이 유행했었다. 그러나 32비트 이후부터는 방법론이 이렇게 더 고차원적으로 확 바뀌었다.;;

Posted by 사무엘

2020/06/28 08:36 2020/06/28 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1767

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

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

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

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

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

사용자 삽입 이미지

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

사용자 삽입 이미지

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

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

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

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

사용자 삽입 이미지

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

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

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

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

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

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

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

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

Posted by 사무엘

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

Windows에는 우리가 생각하는 것보다 훨씬 더 세밀한 UI 옵션들이 가득하다.
사용자의 취향과 컴퓨터의 성능을 고려하여 처음에는 옵션으로 도입됐다가 나중에는 그냥 '답정너 무조건 적용'이나 다름없게 바뀐 옵션의 대표적인 예는 "마우스로 창의 위치나 크기를 변경하는 것을 즉시 반영"이 있다.

마우스가 움직일 때마다 큼직한 창을 매번 다시 그리는 건 198, 90년대의 PC 사양으로 감당하기에는 계산량과 부하가 버거운 작업이었다. 그래서 처음에는 XOR 연산으로 그려진 반전 윤곽선만 마우스 궤적을 따라 그려 주다가 왼쪽 버튼이 떼어졌을 때 실제로 반영하는 형태로 move/resize 기능이 구현되었다. 이 동작을 기억하는 분도 계실 것이다.
그러다가 Windows 95에 와서야.. 그것도 Microsoft Plus!라는 확장팩을 설치한 뒤에 '즉시 반영'이 옵션 형태로 추가됐다.

그리고.. Windows XP에서는 타이핑을 시작했을 때 마우스 포인터를 화면에서 잠시 감춰 주는 자잘한 옵션이 추가되었다. 이건 딱히 성능하고 관계가 있는 옵션은 아니다만.. 텍스트 편집 기능을 자체 구현한 프로그램이라면 운영체제 차원에서 저 옵션이 켜졌는지 여부를 확인해서 저 동작을 지원해 줘야 할 것이다.

운영체제 제어판의 프로그래밍 버전 역할을 담당하는 함수는 SystemParametersInfo이다. SPI_GETMOUSEVANISH를 주면 포인터 숨기기 옵션이 켜졌는지 여부를 알 수 있다. 저 함수가 제공하는 거의 100가지가 넘는 옵션 상수들을 살펴보면 Windows의 UI의 변천 내력을 알 수 있다.
그리고 오늘 이 글에서 논하려 하는 것은 바로 그런 자잘한 UI 옵션 중의 하나인 UI state이다.

마소에서는 사용자에게 온갖 정보를 표시하는 것뿐만 아니라, 불필요한 정보를 가능한 한 숨기고 꼭 필요할 때만 표시하는 것에도 많은 관심을 갖고 연구한 듯하다.
그래서 사용자가 키보드 타이핑을 할 때는 마우스 포인터를 숨기고, 반대로 마우스를 주로 사용하는 것으로 보일 때는 키보드 조작과 관련된 시각 요소들을 숨기는 게 낫겠다는 생각을 하게 됐다.

키보드 조작과 관련된 시각 요소라니? 두 종류가 있다. 하나는 메뉴나 대화상자에서 Alt 단축키를 나타내는 글자 밑의 밑줄이고.. 다른 하나는 현재 키보드 포커스를 나타내는 점선 사각형이다.

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

이런 것들은 없는 게 시각적으로 깔끔하며, 사실 맥OS에는 저런 게 전혀 존재하지 않는다. 하지만.. 걔네들도 키보드 조작에 도움을 주는 정보이니 하루아침에 싹 없애 버릴 수는 없다.
그러니 절충안은.. 일단 숨겼다가 필요할 때만 표시하는 것으로 귀착된다.

하긴, 메모장이나 날개셋 편집기처럼 운영체제의 표준 메뉴를 사용하는 프로그램들을 살펴보면.. "파일(F) 편집(E)" 같은 항목에서 F, E에 쳐진 밑줄도 평소엔 보이지 않다가 사용자가 Alt를 눌렀을 때에만 표시되는 걸 볼 수 있다. 뭐, 요즘은 아예 메뉴가 통째로 보이지 않다가 사용자가 Alt를 눌렀을 때만 표시되는 프로그램도 있지만 말이다.

그리고 아래아한글은 대화상자의 단축키들이 Alt를 누르고 있는 동안만 잠깐 보이고, Alt에서 손을 떼는 순간 사라진다.
MS Office 프로그램은 문서 편집 화면에서 Alt를 단독으로 눌렀다가 떼면 리본의 기능들에 할당된 단축키들이 표시되는데, 이건 대화상자보다는 메뉴의 단축키 동작을 흉내 낸 것에 가깝다.

UI state는 저런 것들과 완전히 같지는 않지만 비슷한 상태와 동작을 다룬다.
Windows에서 돌아가는 모든 윈도우들은 UI state라는 숫자 형태의 속성을 갖는다. 이걸 얻어 오려면 자기 윈도우에 대해서 WM_QUERYUISTATE 메시지를 보내면 된다. 그럼 운영체제가 답변해 준다(wParam, lParam 없음. 리턴값만 확인하면 됨). 딱히 우리가 customize할 필요가 없는 메시지이니, SendMessage 대신 그냥 DefWindowProc에다가 바로 줘 버려도 된다.

리턴값은 비트 플래그 형태이다. 포커스 테두리를 그리지 말 것을 지정하는 UISF_HIDEFOCUS (1), 그리고 단축키 밑줄을 그리지 말 것을 지정하는 UISF_HIDEACCEL (2)를 살펴보면 된다.
Windows XP부터는 UISF_ACTIVE (4)라는 것도 추가됐다고 하지만 개인적으로는 의미나 용도를 전혀 모르겠다. “A control should be drawn in the style used for active controls.”라는 설명만 봐서는 이해가 잘...

static/label 컨트롤은 자체적인 포커스가 없으니 Alt 단축키 밑줄만 신경 쓰면 될 것이고, 아이템을 표시하는 리스트박스, 트리 컨트롤 같은 물건은 포커스 테두리만 신경 쓰면 될 것이다.
그에 비해 버튼(push, radio, checkbox 모두)은 단축키 밑줄과 포커스 테두리를 모두 관리해야 할 것이다. 반대로 에디트 컨트롤은 저런 걸 신경 쓸 필요가 전혀 없을 것이고..

포커스 테두리야 DrawFocusRect 함수를 이용하면 간편하게 그릴 수 있고, &가 섞인 문자열에 대해서 단축키 밑줄을 같이 그리거나 생략하거나(DT_HIDEPREFIX) 심지어 단축키 밑줄만(DT_PREFIXONLY) 그리는 건 DrawText의 여러 플래그들을 사용해서 간편히 구현 가능하다.

그런데 사용자가 대화상자에서 alt나 tab(포커스 테두리) 같은 글쇠를 누르면 그 대화상자 내부의 컨트롤 윈도우들은 감춰 놨던 보조 요소들을 표시하도록 UI state가 바뀌게 된다. tab은 포커스 테두리만 건드리지만 alt는 포커스 테두리와 단축글쇠 밑줄을 모두 건드린다. 한번 표시된 보조 요소들은 다시 숨겨지지 않는다.

이렇게 UI 상태가 바뀌었음을 나타내는 메시지는 바로 WM_UPDATEUISTATE이다. 얘는 우리가 생성하는 게 아니라 운영체제가 보내 주는 메시지이다. 어떤 플래그가 켜지거나(UIS_SET) 꺼졌는지(UIS_CLEAR)를 wParam 값을 통해 알 수 있으니 자세한 사항은 검색해서 구체적인 스펙을 찾아보면 된다.

이 메시지를 DefWindowProc에다가 넘겨 주면 우리 윈도우의 UI state값이 그렇게 변경되며, 동일 메시지가 우리의 child 윈도우들로 전파된다. 다시 말해 WM_QUERYUISTATE의 리턴값은 WM_UPDATEUISTATE를 DefWindowProc에다 요청하기 전과 후가 서로 달라진다는 것이다.
이 메시지를 받았을 때 우리 윈도우가 해야 할 일은, 우리 윈도우의 외형 중에서 그런 UI state의 영향을 받는 게 있는 경우 해당 부분을 다시 그리는 것이 전부이다. 해당사항 없으면 그냥 무시하면 된다.

alt와 tab이야 대화상자에서의 공통 단축글쇠이다. 이때 윈도우의 UI state를 변경하는 것은 IsDialogMessage 함수가 알아서 처리하는 것이라고 쉽게 유추할 수 있다.
그것 말고 우리 윈도우 내부에서도 자체적으로 UI state를 변경해 줘야 할 때가 있다. 사용자가 화살표 키 같은 걸 눌렀을 때 말이다. 이때는 WM_CHANGEUISTATE라는 메시지를 나 자신에게 보내면 된다. wParam에다가는 WM_UPDATEUISTATE와 동일한 스타일로 변경된 플래그를 주도록 한다.

DefWindowProc에서는 이 메시지를 부모 윈도우로 보낸 뒤, 최상위 윈도우에서는 메시지를 WM_UPDATEUISTATE로 바꿔서 자식들로 다시 전파한다. 이런 절차를 거쳐서 대화상자에 있는 모든 윈도우들의 UI state가 동기화된다.

지금까지 얘기했던 것들을 정리하면 이렇게 요약된다.
UI state를 인식해서 동작하는 컨트롤을 직접 구현하고 싶은 경우, WM_QUERYUISTATE를 호출하면 자신의 UI state 값을 얻을 수 있다. 그리고 WM_UPDATEUISTATE가 왔을 때 적절히 화면을 다시 표시하면 된다. 그리고 자기 자신이 UI state를 변경하고 싶은 경우, WM_CHANGEUISTATE를 보내면 된다.

모든 윈도우 메시지들은 DefWindowProc으로 가도록 하면 된다.
대화상자의 경우, 마우스 클릭으로 명령을 내려서 연 경우 저런 보조 요소들이 숨겨진 상태로 시작하며, 그렇지 않고 키보드로 열었다면 이들이 표시되어 있다.

이런 UI state 변경 메시지들과 관련 기능들은 Windows 2000에서 최초로 도입됐다. 9x/ME/NT4 시절에는 존재하지 않았다는 뜻이다. layered window, 마우스 포인터 아래의 그림자, fade out되며 사라지는 메뉴도 다들 2000에서 도입된 기능이다. 2000이 XP 같은 테마는 없었지만 그래도 소소하게 바뀐게 많다는 걸 알 수 있다.

이런 동작은 SystemParametersInfo(SPI_GETKEYBOARDCUES)에 값이 설정돼 있을 때만 행해진다. 제어판에서는 "Alt 키를 누를 때까지 키보드 탐색에 사용할 문자 숨기기"라는 이름의 옵션으로 존재한다. 이게 꺼져 있으면 UI state는 언제나 0으로 고정되며, WM_UPDATEUISTATE 같은 메시지가 오지 않는다.
처음에는 얘의 명칭이 SPI_GETMENUUNDERLINES이었다. 하지만 보다시피 단축키 밑줄뿐만 아니라 포커스 테두리 같은 요소도 추가되면서 명칭이 더 범용적인 'keyboard cue'라고 수정되었다.

이 기능 내지 API와 관련된 본인의 생각을 두 가지 정도 첨언하고 글을 맺도록 하겠다.

1.
라틴 알파벳처럼 음소 풀어쓰기 형태이고 키보드 글쇠와 글자가 일대일 대응하는 문자라면.. 간단하게 해당 문자를 곧장 단축글쇠로 지정해서 아래에 밑줄을 쳐 주면 된다. 하지만 한글· 한자 같은 문자는 그렇지 못하기 때문에 문자열 뒤에 단축글쇠를 별도로 추가해 줘야 한다. 파일(F) 편집(E)처럼 말이다.

이런 환경에서는 단축글쇠를 숨기려면 밑줄만 숨기는 게 아니라 (F), (E)를 통째로 감춰 버리는 게 훨씬 나을 것이다. 하지만 이건 문자열의 내용과 길이를 통째로 변경해야 하니 좀 난감할 수도 있다. 단축글쇠 영역이 기존 UI 문자열의 폭을 건드리지 않도록 별도로 돌출된 툴팁 같은 데다 표시해야 할 것이다.

2.
지금까지 글을 읽은 분이라면 눈치 채시겠지만,
UI state라는 건 앞서 언급했던 라벨, 버튼, 리스트 같은 공용 컨트롤이나 그에 준하는 물건을 새로 구현하지 않는 한.. 프로그래머가 접하거나 신경 쓸 일이 거의 없는 개념이다.
한 대화상자 아래에서 컨트롤들이 UI state가 제각각 달라야 할 일도 없고, 사용자가 WM_***UISTATE 메시지를 DefWindowProc에다가 전달하지 않고 동작을 override해야 할 일도 없다.

사용 패턴이 뻔히 정해져 있고 자주 쓰일 일도 없음에도 불구하고 관련 메시지가 안 그래도 공간이 부족한 시스템 메시지 영역에 3개나 들어가 있는 건 개인적으로 생각하기에 좀 낭비인 것 같다.
UI state는 그냥 GetWindowLongPtr 함수로(GWL_UISTATE) 얻게 해도 되고, WM_CHANGEUISTATE는 메시지 대신 함수가 담당하게 했어도 될 것 같다. 아니면 그 메시지조차도 SetWindowLongPtr로 대체하고 말이다.

내가 보기에 메시지 형태가 꼭 필요한 건 WM_UPDATEUISTATE뿐인 것 같다.

Posted by 사무엘

2020/05/17 08:32 2020/05/17 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1752

Windows에는 문자열을 입력받는 일반 에디트 컨트롤뿐만 아니라 글자마다 서로 다른 서식(글꼴, 크기, 진하게/이탤릭, 탭, 문단 정렬, 하이퍼링크..)을 주고 그림이나 표를 집어넣을 수도 있는 리치(rich) 에디트 컨트롤이라는 게 있다.

그야말로 소형 워드 프로세서가 통째로 윈도우 컴포넌트로 만들어진 셈이니 이건 굉장한 혁신이었다. 심지어 특정 용지 크기에 맞게 위지윅(장치 독립 레이아웃)을 지원하기 위해 기준으로 참조할 DC를 지정하는 기능도 있고.. 아예 윈도우 없이 에디팅 엔진의 동작만 뽑아서 쓰라고 windowless rich edit control이라는 라이브러리도 제공된다. 이 정도면 작정하고 굉장히 세심하게 만들어진 셈이다.
Windows의 기본 예제 프로그램 중 하나인 워드패드가 얘를 써서 만들어진 것으로 유명하며, 초창기에는 Visual C++에 워드패드의 소스가 통째로 제공되기도 했다.

얘의 내부 자료구조는 RTF라는 파일 포맷으로 제정되어서 마소뿐만 아니라 애플 같은 타 회사에서도 쓰기 시작했다. 단적인 예로 macOS의 TextEdit도 이 포맷을 지원한다.
다만, RTF는 HTML이라는 완벽한 상위 호환이 등장하면서 존재감이 굉장히 묻혀 버린 감이 있다. 당장 도움말만 해도 16비트 Windows 시절에는 RTF 기반의 hlp였지만 곧장 HTML 기반으로 대체됐으니 말이다.

상업용 워드 프로세서보다는 기능이 빈약해도 리치 에디트도 엄연히 워드 프로세서에 준하는 물건이니.. 얘는 단독으로 덩치가 굉장히 컸다. 공용 컨트롤 comctl32 패밀리의 멤버 형태로 제공되지 않았으며, 자신만의 전용 DLL과 버전업 체계를 갖추고 있다. 게다가 역사적으로 형태도 몇 차례 바뀌었다.

초창기 1.0은 riched32.dll이었고 윈도우의 공식 클래스 이름은 RICHEDIT였다. Windows 95와 함께 제공되었다.
그러다가 리치 에디트 2.0은 riched20.dll로 바뀌고 클래스 이름도 RichEdit20A 또는 W로 바뀌었다. 짐작하다시피 이때 유니코드가 최초로 지원되기 시작했고 다단계 undo도 지원되기 시작했다. 저 둘은 텍스트 에디터를 밑바닥부터 다시 만들어야 할 명분이 충분한 대공사가 맞다. 얘는 Windows 98과 함께 제공되었다.

나중에 Windows 2000/ME 타이밍 때는 3.0이 나왔는데, 3.0은 프로그래머의 입장에서 API가 바뀐 것이 전혀 없이 2.0을 상위 호환 명목으로 아주 부드럽고 자연스럽게 대체하게 됐다. 그리고 기존 1.0의 생성 요청조차도 그냥 3.0 엔진을 기반으로 호환성 모드로 동작하게 바뀌었다.
지금도 Windows의 system32 디렉터리를 가 보면 riched32.dll은 있긴 하지만 크기가 달랑 10KB밖에 되지 않는다. 실질적인 기능은 riched20.dll에서 수행되기 때문이다.

그랬는데 수 년 뒤, Windows XP sp1에서 리치 에디트 컨트롤은 형태가 또 바뀌었다. 목적은 TSF를 지원하기 위해서다. 얘 역시 내부의 모든 동작을 저 스펙에 맞게 수정해야 하는 엄청난 대공사였다.
얘는 모듈 이름이 영 생소한 msftedit.dll로 바뀌고, 버전도 공식적으로는 4.1이지만 클래스 이름은 RICHEDIT50W이라고 정해졌다. 어디서는 4.1이었다가 저기서는 5라고 표기하면서 혼란스럽다.

리치 에디트 컨트롤은 이렇게 두 번 격변을 거친 뒤에는 딱히 단절적인 변화 없이 지금까지 전해져 오고 있다. MFC에서는 리치 에디트 컨트롤을 초기화하는 AfxInitRichEdit() 계열의 전용 함수를 두고 있다. 2와 5가 붙은 버전도 있다.
그래도 일반적인 대화상자에서 리치 에디트 컨트롤을 집어넣어야 할 일은 그리 많지는 않을 것이며, 굳이 넣더라도 서식이 동원된 문서나 데이터를 “읽기 전용”으로 표시하기 위해서일 것이다.

Visual C++ IDE의 리소스 에디터가 지원하는 것은 버전 2 (사실상 3)에 머물러 있다. 굳이 버전 5를 집어넣으려면 custom control을 삽입해서 RICHEDIT50W를 수동으로 지정해야 한다.
그래도 Visual C++ 201x대의 최신 MFC는 CRichEditView 클래스에 대해 버전 5를 집어넣게 돼 있다. 하긴 4.1인지 5인지 최신 버전이 나온 지가 이미 10년이 넘었는데, 진작에 지원했어야지..

5.0의 가장 큰 존재 의의라 할 수 있는 TSF를 사용하기 위해서는 SES_USECTF 스타일을 지정하는 코드 단 한 줄만을 실행해 주면 된다. SendMessage(hRichEdit, EM_SETEDITSTYLE, SES_USECTF, SES_USECTF)
글쎄, TSF를 제대로 지원하려면 원래는 응용 프로그램에서 COM을 초기화하고 message loop에도 TSF 오브젝트에다가 선처리를 먼저 맡기는 등 해야 할 일이 많다. 이 때문에 날개셋 편집기는 TSF 사용 여부 옵션을 변경한 것이 프로그램을 재시작해야만 적용된다. 그걸 다 무시하고 일반 앱에서 이렇게 간단하게 TSF 지원이 정말 가능한지는 잘 모르겠다.

이걸 해 주면 리치 에디트 컨트롤에서 IME에서 단어 단위 한자 변환이 되며, 날개셋의 경우 다른 고급 특수 기능들도 모두 아무 제약 없이 사용할 수 있다.

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

그 밖에 리치 에디트 컨트롤이 사용 측면에서 기존 에디트 컨트롤과 다른 점은 다음과 같다.

  • 기존 에디트 컨트롤은 단일 배열 버퍼 기반이지만 리치 에디트는 문자열의 연결 리스트 기반으로, 처음부터 대규모 텍스트 편집에 최적화돼 있다. Windows 9x 시절에는 기존 컨트롤은 편집 가능한 텍스트의 크기도 64K 남짓으로 제한돼 있었지만 리치는 그런 한계가 없다.
  • 리치 에디트 컨트롤은 기존 에디트 컨트롤과 달리, 자체적인 우클릭 메뉴가 없다. 우클릭 이벤트 때 할 일은 전적으로 부모 윈도우의 동작에 달려 있다.
  • 기존 에디트 컨트롤은 텍스트의 드래그 드롭을 지원하지 않지만 리치는 지원한다.
  • 기존 컨트롤은 블록이 언제나 짙은 파랑 highlight색으로만 표시된다. 그러나 리치 에디트는 그냥 반전색 또는 요즘 유행인 옅은 파랑으로 표시되며, 사용자 정의를 할 수 있다.
  • 리치는 트리플 클릭(3연타...)으로 텍스트 전체를 선택할 수 있다. 기존 컨트롤은 그런 동작이 지원되지 않는다.

서로 지향하는 목표와 설계 방식이 생각보다 많이 차이가 난다는 걸 알 수 있다. 에디트 컨트롤을 두 종류 따로 만들 만도 하다.
리치 에디트 컨트롤의 다른 사용법들이야 기존 문서를 참고하면 되니 여기서 다룰 필요가 없다. 이 글에서는 역사, TSF 지원, 그리고 한 가지 더.. 중요하지만 다른 문서에서 다루지 않는 특성을 하나 더 다룬 뒤 글을 맺도록 하겠다. 바로.. 경계 테두리이다.

리치 에디트 컨트롤은 공용 컨트롤 계열의 물건이 아니다. 그래서 그런지 공용 컨트롤 6 테마가 적용되었더라도 경계 테두리가 일반 에디트 컨트롤 같은 새끈한 모양으로 안 나오고 그냥 고전 테마의 투박한 모양으로 그려진다. 위의 스크린샷에서 보는 바와 같다. 어찌 된 영문일까? 답을 말하자면 상황이 좀 복잡하다.

윈도우 스타일 중에는 WS_BORDER (검고 가는 테두리), WS_DLGFRAME (버튼 같은 볼록 두툼한 테두리), WS_EX_CLIENTEDGE (오목 두툼한 테두리), WS_EX_STATICEDGE (오목 가는 테두리) 처럼 운영체제 차원에서 윈도우 주변에 non-client 영역을 확보하고 테두리를 치는 스타일들이 몇 가지 있다.

여기서 볼록이라 함은 좌측과 상단은 밝은 계열, 우측과 하단은 어두운 색인 테두리를 말하며, 오목은 순서가 그 반대이다. WS_DLGFRAME(볼록 테두리)을 지정하면 대부분의 다른 테두리 스타일들이 무시되지만, 그래도 WS_EX_CLIENTEDGE와 동시 지정은 가능하다. 그러면 꽤 흥미로운 테두리가 만들어진다. 이 역시 위의 스크린샷에서 묘사된 바와 같다.

이 테두리가 그려지는 모양은 테마의 적용 여부와 무관하게 언제나 동일하다. 그렇기 때문에 특별히 하는 일이 없다면 원래는 리치 에디트 컨트롤처럼 투박하게 그려지는 게 맞다.

테마가 적용된 공용 컨트롤 6들은 WS_EX_CLIENTEDGE(오목하고 두툼한 테두리)가 존재할 경우, WM_NCPAINT 메시지를 자체 처리하여 DrawThemeBackgroundEx 같은 theme API를 호출해서 테두리를 그린다. 자세히 보면 심지어 포커스를 받았을 때와 그렇지 않을 때 테두리 색깔이 달라지는 것도 확인할 수 있다. 하지만 리치 에디트 컨트롤은 저런 처리를 안 하기 때문에 테두리가 고전 테마 모양 그대로인 것이다.

그러니 컨트롤 자신이 테두리를 제대로 그리지 않으면 응용 프로그램이 강제로 그려 주는 수밖에.. 리치 에디트 컨트롤의 테두리 미관을 개선하려면 해당 컨트롤을 서브클래싱 해서 WM_NCPAINT 처리를 직접 하는 것 말고는 선택의 여지가 없다. 이것도 뭔가 운영체제 차원에서의 자동화 절차가 필요해 보인다.

본인이 이런 테두리 그리기와 관련해서 알게 된 굉장히 놀라운 사실이 있다.
오늘날 Windows에서 대화상자를 꺼내는 DialogBox, CreateDialog 계열 함수들은 대화상자 리소스에서 WS_BORDER이라고 지정된 스타일을 무시한다. 그걸 무조건 WS_EX_CLIENTEDGE로 치환해 버린다.

오목하고 두툼한 테두리는 대화상자 내부에서 사용자가 뭔가 아이템을 선택하고 문자를 입력하는 '작업 공간'을 나타내는 시각 피드백이다. 그에 비해 볼록/오목 효과가 없이 그냥 flat한 검정 단색 테두리(WS_BORDER)는 대화상자에 회색 입체 효과가 없던 Windows 3.x 시절 비주얼의 잔재로 여겨진 것이다.

어쩐지 옛날에도 WS_BORDER이랑 WS_EX_CLIENTEDGE가 차이가 없는 것 같았는데 그땐 그저 그러려니 하고 넘겼었다. 관계가 정확하게 저렇다는 걸 본인도 이제야 직접 조사해 보고 알게 됐다. 대부분의 경우 WS_BORDER는 그냥 WS_EX_CLIENTEDGE로 포워딩 되는 호환성 옵션으로 전락했다.

다만, 테마가 적용된 뒤에는 윈도우의 외형이 다시 옛날 같은 flat 스타일로 돌아간지라.. 검정 단색 테두리가 회색 단색 테두리로 바뀌었을 뿐이다. 그래서 볼록/오목 효과가 역으로 오래되고 촌스럽게 보이는 촌극이 빚어져 있다. 역사는 이런 식으로 돌고 돈다! =_=;;;

이상이다. 그러고 보니 리치 에디트는 최신 버전인 5(또는 4.1)에 대해 공용 컨트롤 6처럼 side-by-side assembly를 적용하는 게 충분히 일리가 있음에도 불구하고 전혀 그런 조치가 취해지지 않았다. 그렇기 때문에 얘는 사용자가 DLL과 윈도우 클래스 이름을 달리하는 원시적인 방법으로 버전 구분을 해야 한다.

즉, 리치 에디트는 Windows XP와 동시대에 개발됐음에도 불구하고 같이 개발되던 관련 신기술 두 종류와는 완전히 열외된 셈이다. (테두리 테마, SxS assembly) 아쉬운 대목이 아닐 수 없다. 리치 에디트 팀의 관심사에 든 XP의 신기술은 오로지 TSF뿐이었다.

본인이 개발한 날개셋 한글 입력기에도 자체 에디트 컨트롤과 텍스트 에디터가 있다. 먼 옛날에 2.x에서 3.0으로 넘어갈 때 프로그램이 내부 구조를 다 뜯어고치고 완전히 새로 개발되었는데, 이때 유니코드 기반, 다단계 undo, 그리고 TSF까지.. 리치 에디트 컨트롤이 1에서 2, 3에서 5로 갈 때의 공사를 몽땅 진행했다. 리치 에디트와 비슷하다면 비슷한 전철을 밟은 셈이다.

Posted by 사무엘

2020/04/27 08:35 2020/04/27 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1745

Windows API에는 현재 실행 중인 프로세스 및 스레드의 정체를 알려 주는 GetCurrent[Process/Thread]{Id}라는 함수쌍이 있다. Current 다음에 Process가 오느냐 Thread가 오느냐, 그리고 그 뒤에 Id가 붙느냐 안 붙느냐에 따라 2*2 총 4가지 조합이 존재한다.

뒤에 Id가 붙은 함수는 시스템에서 실행 중인 모든 프로세스 및 스레드를 유일하게 식별하는 32비트 정수형(DWORD) 번호를 되돌린다. 그리고 그게 없으면 이들 함수는 HANDLE이라는.. 성격이 좀 다른 번호를 되돌린다. 명목상 포인터 크기와 동일하지만, 64비트에서도 얘 역시 여전히 사실상 32비트 크기만치만 활용된다.

HANDLE로는 ID처럼 프로세스나 스레드를 유일하게 식별할 수 없는 걸까? HANDLE과 ID의 차이는 무엇이며, 둘의 구분은 왜 존재하는 걸까?

답을 얘기하자면 HANDLE은 ID 이상으로 더 무겁고 복잡하며 상태 의존적인 별개의 존재이다.
HANDLE은 일단 커널 오브젝트이다. 값을 얻기 위해 뭔가 운영체제로부터 자원을 할당받고 나중에 반납을 해야 한다. 사용한 뒤에는 마치 열었던 파일을 닫듯이 CloseHandle을 호출해서 닫아 줘야 한다. 단순 ID에는 이런 과정이 필요하지 않다.

그리고 이 HANDLE은 뮤텍스나 이벤트 같은 동기화 오브젝트 중의 하나이다. WaitForSingleObject 함수에다 넘겨서 이 프로세스나 스레드의 실행이 끝날 때까지 기다리는 용도로 사용할 수 있다.
심지어 HANDLE이 가리키는 그 프로세스나 스레드가 실행이 종료됐더라도 그 핸들 자체는 정식으로 닫아 주기 전까지는 여전히 살아 있다.

또한, 값이 다른 여러 HANDLE이 동일한 프로세스나 스레드를 참조할 수 있으며, 동일한 그런 개체에 대해서도 한번 닫았다가 핸들을 다시 얻은 리턴값은 달라질 수 있다. 마치 메모리 할당 함수의 실행 결과처럼 말이다. 그러므로 프로세스나 스레드 실체만을 유일하게 식별하려면 ID를 살펴보는 게 정답이다.

끝으로 결정적으로... GetCurrent**** 함수는 핸들이긴 하지만 좀 특이한 값을 되돌린다. 바로.. 그 함수를 호출하는 프로세스 및 스레드 자기 자신을 의미하는 고정된 상수만을 되돌리기 때문이다. IP 주소로 치면 localhost처럼 말이다. 이 상수 핸들은 CloseHandle을 하지 않아도 된다.

자기 자신 프로세스를 의미하는 상수는 -1 (0xFFFFFFFF)이고, 자기 자신 스레드를 의미하는 상수는 -2 (0xFFFFFFFE)이다.
이 정도면 #define HANDLE_CURRENT_PROCESS 이런 식으로 함수 대신 그냥 매크로 상수로 박아 넣어도 되고.. 프로세스 핸들과 스레드 핸들이 서로 섞여 쓰일 일도 없으니 -1과 -2로 구분조차 하지 않아도 된다. 하지만 Windows API가 처음 만들어질 때 그렇게 되지는 않았다.

비록 저 함수가 고정된 상수만 되돌린다는 것이 공공연한 비밀에 20년이 넘는 관행이 돼 버리긴 했지만, 미래에 이 함수의 리턴값이 바뀔 수도 있으니 꼬박꼬박 함수를 호출해서 핸들값을 사용해 달라는 것이 마소의 방침이다.
Windows NT가 개발된 지 30년이 돼 가는 지금 시점에서 이들 함수의 리턴값이 달라질 가능성은 사실상 0으로 수렴했지만.. 그래도 세상일은 알 수 없으니 말이다.

자기 자신 말고 타 프로세스의 유효 핸들은 아무래도 기존 프로세스 ID로부터 얻는 게 제일 직관적이다. 프로세스 ID는 프로세스 전체를 조회하는 EnumProcesses로부터 얻을 수도 있고 윈도우 핸들로부터 GetWindowThreadProcessId를 호출해서 얻을 수도 있다. 당연히 그 윈도우를 생성한 주체를 얻는다.

그렇게 해서 얻은 프로세스 ID에 대해서 OpenProcess를 호출하면 프로세스 핸들을 얻을 수 있다. 그럼 이 핸들에 대해서는 프로세스를 강제 종료하는 Terminate**** 함수, 아까처럼 실행이 끝날 때까지 기다리는 WaitFor**** 함수, 얘가 64비트인지 여부를 얻는 IsWow64Process, 실행 파일 이름을 얻는 GetModuleFileNameEx 등.. 할 수 있는 일이 몇 가지 있다.

CreateProcess 함수는 새로운 프로그램을 실행하면서 PROCESS_INFORMATION 구조체에다가 새 프로세스의 핸들과 ID, 그리고 primary 스레드의 핸들과 ID 이렇게 네 정보를 모두 쿨하게 되돌려 준다. 그러니 좋긴 하지만.. 이것들을 사용하지 않는다면 즉시 CloseHandle도 잊지 말고 해 줘야 resource leak를 방지할 수 있다.

스레드에 대해서도 프로세스와 비슷하게 스레드 ID로부터 유효 핸들을 얻는 OpenThread라는 함수가 있다. 하지만 저 함수는 OpenProcess와 달리, 본인이 지난 수십 년의 프로그래밍 커리어 전체를 통틀어 한 번도 사용한 적이 없었다.

일단, 내 코드가 생성한 스레드라면 그냥 CreateThread의 리턴값을 받아 두면 되니, 별도의 방법으로 스레드 핸들을 얻을 필요가 없기 때문이다. 저렇게 스레드 핸들을 얻는 건 무슨 시스템 유틸리티를 만들고 있어서 내가 생성하지 않은 듣보잡 프로세스 내지, 내 프로세스 안에서도 타인의 듣보잡 스레드를 건드려야 할 때나 필요하다. 그리고 그런 일은 일반적으로는 잘 없다.

그리고 스레드 핸들은 그냥 끝날 때까지 대기할 때(WaitFor***), 아니면 우선순위를 조절할 때(SetThreadPriority) 정도..?? 프로세스 핸들만치 무슨 정보를 얻고 쓸 일이 별로 없기도 하다. 그러니 자기 자신을 가리키는 가짜 핸들을 얻는 GetCurrentThread도 쓸 일이 거의 없다. 강제 종료 역시 TerminateThread는 TerminateProcess보다 훨씬 더 위험하며 훨씬 더 비추되는 짓이고 말이다.

프로세스나 스레드의 실행이 종료되는 것하고 해당 프/스를 가리키던 핸들이 완전히 해제되는 것은 완전히 별개의 일이다. 심지어 Terminate*를 호출해서 강제로 실행을 중단시켰더라도 거기에다 넘겨줬던 핸들은 CloseHandle을 따로 해 줘야 한다.

AttachThreadInput이라든가 SetWindowsHookEx 같은 UI 함수에서 스레드를 지정할 때는 그냥 간편하게 ID를 지정하는 것만으로 충분하다. 굳이 핸들값을 주지 않아도 된다.
이런 여러 이유들로 인해 스레드 핸들은 프로세스 핸들보다 쓰이는 빈도가 낮다.

이상이다.
이런 것들은 Windows 프로그래밍에서 완전 기초 내용이다. 하지만 기본기 복습 차원에서 프로세스와 스레드, 그리고 핸들과 ID의 관계를 이렇게 한번 정리해 놓고 싶다는 생각이 코딩 중에 문득 들었다.

Posted by 사무엘

2020/03/15 08:34 2020/03/15 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1728

GUI 표시 용도로 쓰이는 폰트

컴퓨터에서 폰트, 글꼴이라는 건 말 그대로 사람이 읽는 문자를 찍는 용도로 널리 쓰인다.
그런데 그것만이 전부가 아니다. 문자는 그 자체로 생각보다 굉장히 정교한 그래픽 데이터이며, 그걸 빠르게 보기 좋게 찍어 주는 소프트웨어는 매우 강력한 벡터 그래픽 엔진에 속한다. 이걸 다른 그래픽 요소를 출력하는 용도로 얼마든지 활용할 수 있다.

1. Marlett

단적인 예로 Windows의 경우.. 95부터 7까지 15년 가까이 존속했던 '고전 테마'의 GUI 요소들은 상당수가 글꼴 출력이었다.
체크 박스의 체크 표시, 라디오 버튼의 동그라미, 창의 최소화/최대화/닫기 버튼, 콤보 상자 오른쪽에 붙은 ▼ 모양 말이다.

이것들의 정체는 Marlett이라는 트루타입 글꼴에 들어있는 글립이다. 다음 그림은  이 글꼴에 들어있는 글립들을 표시한 것이다. 모양들이 다들 기억에 남아 있고 익숙할 것이다.
하드코딩된 비트맵이 아니라 이렇게 윤곽선 글꼴로 처리하면 다양한 크기에 대응 가능하다는 이점이 생긴다.

사용자 삽입 이미지

겨우 27,724바이트밖에 하지 않는 아담한 크기이며, 제공되는 글립은 저게 전부이다. Windows 95내지 NT4 이래로 내용이 한 번도 바뀐 적이 없는 듯하다.
라디오 버튼은 잘 알다시피 흑백 음영이 져 있는데, 왼쪽 절반은 흰색으로, 오른쪽 절반은 검정으로 그려서 구현했다는 것을 쉽게 유추할 수 있다. 더구나 응용 프로그램이 크기 조절이 가능한 상태라고 우측 하단에 표시되는 빗금무늬도 알고 보니 이 글꼴의 글자였다.

그런데 어지간한 프로그램의 글꼴 목록에서 Marlett이라는 이름의 글꼴을 본 적이 있는 분은 별로 없을 것이다. 얘는 일반적인 한글 내지 영문용 글꼴이 아니라 Wingding 같은 심벌 전용 특수 글꼴이기 때문에 그렇다.

지금이야 저런 dingbat 심벌들을 모두 독립된 코드값을 부여해서 새로운 문자로 만들어 주는 시대이지만, 옛날에는 유니코드는커녕 2바이트 문자 코드조차도 생소했다. 그러니 기성 문자 코드에 없는 custom 그림문자들은 아스키 코드 번호에다가 글꼴만 달리해서 찍는 게 불가피한 관행이었다. 또한, 그런 용도로 만들어진 심벌 전용 글꼴은 언어별로 필터링을 해서 글꼴 목록을 출력하는 프로그램에서는 이름이 조회되지 않았던 것이다.

Marlett은 MS Sans Serif, System (-_-;; ), Wingding처럼 16비트 Windows 시절부터 존재했던 글꼴도 아니니.. 이름이 더욱 생소할 수밖에 없다. 이 글꼴의 존재를 확인하려면 '문자표'처럼 진짜로 원초적인 글꼴들을 몽땅 나열해 주는 프로그램을 써야 한다.

아직도 잊혀지지 않는데.. 과거에 Windows 95는 알 수 없는 이유로 인해 주기적으로(몇 달 간격?) 글꼴 시스템이 맛이 가곤 했다. 그래서 창의 최소/최대화 버튼처럼 Marlett 글꼴을 써서 출력되던 GUI 요소들이 깨져서 Small Fonts 기준의 숫자나 알파벳으로 바뀌어 찍혔다. 원인은 잘 모르겠지만 운영체제를 강제로 '안전 모드'로 몇 번 재부팅을 해 주면 문제가 해결되었다.

그리고 요즘은 반대로.. 일반적인 숫자와 문자로 찍혀야 할 문자열들이 Marlett 기준으로 엉뚱하게 깨져서 찍히는 문제가 발생하기도 하는가 보다. 난 직접 본 적은 없다.

사용자 삽입 이미지

오늘날의 Windows에서는 고전 테마가 공식적으로는 사라졌지만, 고전 테마 형태의 GUI 요소를 그려 주는 DrawFrameControl 같은 함수는 여전히 존재한다. 그게 없어질 수는 없다. 그러니 Marlett 폰트도 legacy 명목으로 언제까지나 남아 있을 것으로 보인다.

2. 설치 프로그램/부팅 화면에서 쓰이는 폰트

운영체제는 GUI를 제공하는 셸이란 걸 갖추고 있는데, 사실은 그 전에 운영체제의 설치 프로그램도 동일한 셸을 기반으로 GUI를 제공해야 한다.
그렇기 때문에 Windows 원본 디스크의 내부를 뒤져 보면 *.ttf 파일이 존재하는 걸 볼 수 있다. Windows를 다 설치했을 때와 동일한 규모는 아니고, 그냥 설치 프로그램 GUI를 표시하는 데 필요한 정도만 존재한다.

Windows XP쯤부터는 쌩 텍스트 모드라는 게 없어졌다. 그래서 시꺼먼 배경의 명령 프롬프트 전체 화면에서도 한글이 언제나 16*16 비트맵 글꼴로 찍힌다.
그리고 Vista부터는 16*16 비트맵 글꼴이 찍히는 명령 프롬프트 전체 화면이라는 게 없어졌다. 그 대신, 부팅이나 설치 중간에 시꺼먼 배경의 전체 화면이 필요할 때 표시되는 글자들이 16픽셀이 아니라 24픽셀 이상으로 커졌다.

사용자 삽입 이미지

이것들은 다 kor_boot라는 트루타입 글꼴에 포함돼 있는 비트맵 글립이다. 다만, 영문은 설치 때는 똑같은 굴림 계열이고, 설치 후의 부팅 관리자에서는 Lucida Console로 차이가 나는 것 같다.
그리고 하나 더..
Windows 8부터는 운영체제의 부팅이나 설치/업데이트 작업이 진행 중일 때 동그라미 5개가 뱅글뱅글 돌아가는 애니메이션이 나오기 시작했다.

사용자 삽입 이미지

이 애니메이션의 기술 기반은 gif나 avi 따위가 아니라.. 역시 글꼴이다!
malgun_boot라는 글꼴 파일을 보면 U+E000대 사용자 정의 영역에 110프레임이 넣는 각 모양들이 일일이 그려져 있다. 이 글자들을 짧은 시간 동안 연달아 찍게 하면 동그라미 5개가 뱅글 돌면서 들어갔다가 나오는 애니메이션이 그려진다.

위의 움짤은 저 폰트의 글립을 연달아 찍고 화면을 캡처해서 저장하는 프로그램을 본인이 직접 작성하고 돌려서 20분 만에 만든 것이다.

사용자 삽입 이미지

이렇듯, 부팅 때 잠깐 쓰이는 (1) 한글 비트맵 글꼴과 (2) 동그라미 애니메이션용 글꼴이 거론되었는데.. 얘들은 설치 프로그램뿐만 아니라 설치가 완료된 Windows 자체도 사용하는 폰트이다. 하지만 Windows에서 _boot이라는 이름을 가진 폰트 파일이 딱히 조회되지는 않는다.

고전 테마 GUI 글꼴은 앞서 살펴본 바와 같이 Marlett이라고 알려져 있으며, FixedSys, System, MS Sans Serif 같은 옛날 티 팍팍 나는 초 구닥다리 글꼴들도 다 C:\Windows\Fonts에 *.fon이라는 파일 형태로 존재한다. 그럼 저것들은 어디에 콕 짱박혀 있는지, 실체가 어디에 존재하는지가 매우 궁금해진다.

사용자 삽입 이미지

Windows Vista/7부터 마소에서는 Segoe UI라는 이름의 폰트 시리즈를 밀기 시작했다.
처음에는 본편만 있다가 8부터는 Segoe UI Symbol이라는 게 추가됐으며, 얘의 PUA 영역에는 위에서 보다시피 체크박스, 컴퓨터 전원 버튼, 웹브라우저 뒤로-앞으로 버튼, 그리고 좌우상하 방향, 배터리 충전 상태 등...

어지간한 도구모음줄 버튼 아이콘까지 다 폰트 형태로 제공되기 시작했다.
단색으로 단순화됐으면서 크기 조절은 마음대로 되니까.. 폰트가 괜찮은 선택인 셈이다.
원래는 이모지까지 담고 있다가 이모지가 단독으로 워낙 방대해지니 이건 Segoe UI Emoji라는 전용 폰트로 분리되어 나갔다. 한 8.1이나 10쯤부터?

3. 나머지

운영체제뿐만 아니라 응용 프로그램도 자신만의 UI 요소를 간편하고 깔끔하게 출력하기 위해 폰트를 사용한다.
예를 들어 Visual Studio IDE의 경우, 각종 창들을 사용할 때만 툭 튀어나오게 하거나, 아니면 붙박이로 고정(pin)시킬 수 있는데, 그 상태를 나타내는 아이콘이 바로 자체 폰트이다.

초창기 버전인 Visual Studio .NET은 이를 위해 VisualUI라는 폰트를 임시 등록하곤 했다.
아래의 글립들을 보시라. 0x5부터 0x7의 자리에 있는 주사기 비스무리한 아이콘이 바로 저것이다.

사용자 삽입 이미지

.NET의 초기 버전은 자기가 실행돼 있는 동안 VisualUI 폰트가 시스템 전체에 등록되어서 이름을 조회할 수 있었다. 이는 불필요한 side effect이므로 후대 버전에서는 그렇게 되지 않게 바뀌었다.

또한 Outlook도 자체 UI를 표시하기 위한 폰트를 구비하고 있었다. MS Outlook이라는 폰트가 있는데, 아래와 같은 글립만 들어있는 작고 단촐한 폰트이다.

사용자 삽입 이미지

이것도 후대 버전에서부터는 티가 나지 않기 시작했다. 참고로 폰트를 시스템 전체가 아니라 자기 프로세스 내부에만, 더구나 디스크 상의 파일이 아니라 메모리 데이터만으로 간편하게 임시 등록하는 함수는 Windows 2000에서부터 처음으로 추가됐다(AddFontMemResourceEx). 자기들도 프로그램 만들 때 필요하니까 추가한 것이 분명해 보인다.

그럼, 개인적으로 하나 궁금한 것을 예를 들면서 글을 맺도록 하겠다.
저런 기하학적인 UI 이미지 중에는 뭔가 공간이 부족한 환경에서 "나머지도 마저/모두 표시"(Show all / More)라는 의미를 갖고 있는 이중 갈매기(angled bracket) 마크가 있다. >> 요거 말이다. 오른쪽을 가리키고 있는 게 제일 많으며 드물게 아래로 향할 때도 있다.
부등호와 비슷하게 생겼지만 명칭이 부등호는 절대 아니다. 이건 마이너스(빼기)와 dash/hyphen이 다른 것만큼이나 다르며, 번호 #과 조표 #이 다른 것만큼이나 다르다.

사용자 삽입 이미지

이렇듯, 요 마크는 사용자가 심심찮게 접한 적이 있을 것이다.
어째 공교롭게도 Windows XP 시절의 옛날 UI에서 더 눈에 많이 띄는 것 같다만.. 그래도 저 표시가 옛날 UI의 전유물인 것은 아니다.

더구나 Windows 9x 초창기부터 존재했던 물건도 아니다. 그렇기 때문에 의외로 Marlett 폰트에는 존재하지 않는다. 딱 거기 있을 법해 보이는데도 불구하고 말이다.
아래로 향하는 모양은 Windows 2000/ME 시절, 딱 저 personalized menu 때문에 즐겨 쓰였고, 오른쪽으로 향하는 모양도 저렇게 90년대 말이나 2000년대 초부터 등장한 것같다.

본인은 이것도 뭔가 모처의 폰트를 사용해서 출력하는 것이지, 비트맵은 아닐 거라고 생각한다. 하지만 정확한 출처를 발견하지 못했다. comctl32나 shell32 내부에 비트맵으로라도 존재하지 않을까 생각했지만 그런 건 없었다. 그 출처가 궁금하다. 빨리 감기/되감기 버튼처럼 채워진 삼각형이 두 개 붙은 모양은 각종 심벌 글꼴에 아주 흔하게 존재하는 반면에 저 모양은 유난히 눈에 띄지 않더라.

참고로 MFC에서는 하드코딩된 비트맵 형태인 게 맞다. MS Office의 외형을 흉내 내는 기능이 있으니 저런 마크를 출력하는 기능도 응당 있을 텐데.. 소스를 들여다보니 그냥 비트맵이었다. 약간 허무함이 느껴졌다. 명칭은 afxmenuimages.h에 선언돼 있는 IdArrowShowAll (아래), IdMoreButtons (오른쪽)이다.

사용자 삽입 이미지

Posted by 사무엘

2020/01/11 08:33 2020/01/11 08:33
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1704

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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
1407235
Today:
69
Yesterday:
497