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

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 , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1603

« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : ... 21 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2026/10   »
일 월 화 수 목 금 토
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

Site Stats

Total hits:
4198113
Today:
1755
Yesterday:
2133