« Previous : 1 : ... 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : ... 13 : Next »

사용자의 마우스 클릭에 동작하는 GUI 요소들은 일단은 버튼의 down이 아니라 up 시점 때 반응을 한다. 모든 버튼(push, check, radio 공히)이나 하이퍼링크들이 이렇게 동작하며, 우클릭 메뉴도 오른쪽 버튼을 뗐을 때 튀어나온다. 그리고 이 타이밍 때 그 이름도 유명한 BN_CLICKED라는 notify 메시지가 부모 윈도우에 전달된다.

그에 반해, down 때 바로 반응을 하는 것은 스크롤 바나 슬라이더나 스핀(옆의 숫자를 증가시키거나 감소시키는 up-down 컨트롤)처럼 누르고 있는 동안 '연타'의 여지가 있는 버튼, 아니면 어떤 명령이 즉시 실행되는 게 절대로 아님이 보장되는 첫 단계 메뉴(파일, 편집, 보기) 정도로 국한된다.

그런데 이건 어떨까? 버튼인데, 사용자가 누르고 있는 동안만 풍선 도움말이나 다른 추가적인 정보가 튀어나오고, 버튼을 떼면 그것들이 사라진다. down과 up만 감지하면 되고 그 사이에 연타 개념은 없다.
이건 일단 Windows의 표준 컨트롤에는 없는 기능인 듯하다. 하지만 의외로 요런 UI가 쓰인 경우가 존재한다.

가장 먼저 떠오르는 예는 2007 버전이 나오기 전, 과거의 MS Excel이다. 선에 안티앨리어싱이 없는 건 그렇다 치더라도 색깔이 요즘 것에 비해서 우중충하고 덜 예뻐 보인다.

사용자 삽입 이미지

옛날에 엑셀에는 차트 마법사를 통해서 차트를 만들곤 했다. 1단계는 차트의 종류를 고르는 단계였는데, 아래의 길쭉한 버튼을 살포시 누르고 있으면 지금 사용자의 데이터가 어떤 형태의 차트로 표시되는지 preview를 잠시 볼 수 있었다.
버튼을 한번 눌렀다 뗄 때마다 preview 모드와 선택 모드가 toggle 형태로 바뀌는 게 아니라는 점을 주목하자. 상당히 독특한 UI가 아닐 수 없다. 왜 이렇게 만들었을까?

어지간하면 그냥 대화상자의 왼쪽에 차트의 type과 sub-type을 트리 컨트롤 같은 걸로 모두 때려박고, 오른쪽 전체에 preview 화면을 할당해도 될 듯하지만 그렇게 하기에는 오버헤드가 너무 크고, 또 차트의 타입 자체를 그림으로 표시하기 위한 공간도 많이 필요하니 이런 식으로 preview는 필요할 때 잠깐만 볼 수 있게 UI를 만든 듯하다.

참고로 Office 2007과 그 이후부터는 전통적인 차트 마법사가 없어지긴 했지만, 차트의 종류를 선택하는 대화상자 자체는 남아 있다. 단, 이렇게 버튼을 누르고 있는 동안 preview를 잠깐 보는 기능은 없어졌다.

원래 운영체제의 표준 버튼은 아까도 얘기했듯이 눌렀다 뗀 뒤의 BN_CLICKED 이벤트만 있지, 저렇게 눌러진 것에 대한 이벤트는 제공하지 않는다. 그러니 저런 기능을 구현하려면 일반적으로는 윈도우 프로시저를 서브클래싱하여 마우스 좌클릭과 Space/Enter 누름을 감지해서 좀 불편하게 구현해야 한다.
하지만 MS Office 제품 중에 Word와 Excel은 대화상자 컨트롤들을 운영체제 함수 대신 자체 GUI 엔진으로 구현했기 때문에 편법 없이 저런 기능들이 처음부터 자연스럽게 구현이 가능했을 것이다.

그리고 또 다른 예는 아주 최신 프로그램이다. 바로 Internet Explorer 11.

사용자 삽입 이미지

IE는 최신 버전이자 아마 마지막 버전이 될 것으로 보이는 11이 굉장한 쇄신을 한 것 같다. 예전보다야 가벼워지고 속도가 빨라지고, 텍스트 입력란이 TSF A급으로 바뀌고, 굴림체로 찍히던 기본 글꼴이 맑은 고딕으로 바뀌는 등 변화가 많다.

얘는 웹사이트 내부의 입력란에서 ID와 비번을 입력하기 시작했을 때 오른쪽에 자그마한 버튼이 뜬다. ID 입력란의 오른쪽 끝에는 X 버튼이 생겨서 이걸 누르면 ID가 싹 다 지워진다. 물론 지우는 동작은 눌렀다가 '뗐을 때' 행해진다.
그런데, 비번 입력란의 오른쪽에는 눈알 모양의 버튼이 생기며, 이 버튼을 누르고 있으면 동그라미로 표시되는 암호 문자열의 실제 문자열이 잠시 보인다. 이것 역시 마우스 버튼을 떼는 순간 원상복귀된다!

(1) 엑셀의 차트 미리보기와, (2) IE에서 비번 훔쳐보기가 이 동작에 상당히 적절하게 배치된 경우라고 생각된다. peek라는 동작을 표현한다고나 할까. 이것 말고 요런 동작이 유용하게 쓰일 만한 상황이나 이미 적용된 예가 무엇이 있는지 궁금하다.

마우스 포인터를 갖다대고 있는 동안 뭔가 도움말이나 추가 정보가 나타나는 UI는 그리 새삼스러운 물건이 아니다. 이미 20년 전부터 툴팁이라고 불리는 풍선 도움말이 그 역할을 하고 있으니 말이다.
단, 터치스크린에서는 click과 hover가 구분이 없기 때문에 저렇게 가리키고 있는 것을 표현할 수 없으니 누르고 있는 동안만 뭔가 추가 정보를 표시하는 기능이 더욱 필요할 것이다.

이렇게 글을 바로 맺기는 아까우니 MS Office에서만 볼 수 있는 독특한 UI를 두 가지 좀 늘어놓도록 하겠다.

1. 먼저, 탭 컨트롤의 각각에 Alt 액셀러레이터가 붙은 모양이다. 난 워드나 엑셀을 쓸 때마다 늘 신기하다고 생각해 왔다.

사용자 삽입 이미지

Alt 액셀러레이터는 IsDialogMessage라는 대화상자 전용 메시지 처리 함수가 구현해 주는 것이고, Alt는 윈도우 텍스트를 기반으로 한 윈도우당 한 글자씩만 배당된다. 그러므로 저게 정석적으로 가능하려면 각각의 탭 헤더가 마치 라디오 버튼처럼 독립된 윈도우를 구성하고 있어야 하나, 운영체제의 탭 컨트롤은 그런 구조를 하고 있지 않다.

Alt+단축키를 눌렀을 때 탭 컨트롤의 탭이 어떻게든 전환되게 하는 것 자체는 불가능하지 않다. 하지만 그러려면 윈도우 프로시저 수준이 아니라 해당 응용 프로그램의 메시지 loop 차원에다가 예외적인 처리가 필요하다. MFC로 치면 해당 대화상자의 PreTranslateMessage 함수를 굳이 새로 구현해야 한다.
그래서 본인은 MS Office의 저런 프로퍼티 시트 대화상자를 보면 신기하다는 생각이 든다.

2. 그리고, 라디오 버튼을 더블 클릭하면 대화상자가 OK(확인)으로 종료되는 것도 꽤 독특하다.
요즘 Office 프로그램들이야 워낙 기능이 방대하기 때문에 공간을 아끼기 위해 콤보 박스를 주로 쓰지, 라디오 버튼을 보기는 힘들지만.. 대표적으로 '화면 확대'라든가 탭의 종류를 고르는 화면에서 이를 확인할 수 있다.
아.. 그러고 보니 리스트 박스에서 아이템을 더블 클릭했을 때 대화상자가 재량껏 OK로 종료되는 건 생소하지 않다. 하지만 라디오 버튼을...? 한번 생각해 보시기 바란다.

Posted by 사무엘

2015/07/13 08:45 2015/07/13 08:45
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1115

맥 OS는 화면 상단에 붙박이로 메뉴가 있어서 모든 프로그램들이 동일한 메뉴 표시줄을 공유한다. 이게 Windows로 치면 응용 프로그램의 메뉴 겸 작업 표시줄의 시작 메뉴, 그리고 심지어 시스템 트레이의 역할까지 한다.
그러나 Windows는 각각의 창이 자신의 고유한 메뉴 표시줄을 갖는 구조이다. 이건 맥과 Windows가 무려 1.0 시절부터 서로 다르게 설계한 디자인이다.

그래서 Windows에는 응용 프로그램의 기본 메뉴뿐만이 아니라 어떤 창이라면 공통으로 갖는 창 자체에 대한 메뉴도 있는데, 이것을 '시스템 메뉴'라고 한다. 창 자체를 이동하거나 크기를 조절하고, 최소화/최대화를 시키거나 닫는 고정적인 명령들이 있다. 마우스로는 좌측 상단에 있는 해당 프로그램의 아이콘을 클릭하면 되고, 키보드로는 Alt+Space를 누르면 된다.

옛날에 3.x 시절에는 '작업 목록(지금으로 치면 작업 관리자와 비슷)'을 꺼내는 명령도 시스템 메뉴에 있었는데 그건 Windows 95부터 없어졌다. 그땐 시작 메뉴나 작업 표시줄 같은 게 없었기 때문에 Alt+Tab 외에 응용 프로그램간 전환을 하는 방법을 그런 식으로 마련해 놓을 필요가 있었던 것이다.

그런데 창의 시스템 메뉴는 기본 메뉴의 연장선의 성격을 지님과 동시에 한편으로 우클릭 팝업 메뉴의 형태로도 부를 수 있다. Alt나 F10을 눌러서 기본 메뉴를 열어서 좌우 화살표 키를 누르면 기본 메뉴와 더불어 시스템 메뉴로도 순환이 된다.
그러나 창의 제목 표시줄을 우클릭하면 창의 시스템 메뉴만 따로 우클릭 메뉴의 형태로 꺼낼 수 있으며, 이때는 좌우 화살표를 눌러도 기본 메뉴가 표시되지는 않는다. 사실, 시스템 메뉴를 우클릭 메뉴 형태로 꺼내는 건 Windows 95에서부터 추가된 새로운 UI이긴 하다.

그리고 Windows 95에서는 메뉴의 아이템들 중 하나를 default로 지정하여 진하게 강조되어 출력하는 UI가 추가되었다. 당장 시스템 메뉴에서 확인할 수 있는데, 이것은 디자인 컨셉상 우클릭 메뉴에서 쓰라고 도입되었다.
화면에서 어떤 개체를 더블 클릭했을 때 취해지는 default 동작이 해당 개체의 우클릭 메뉴에 포함되어 있다면, 그 동작에 해당하는 메뉴 아이템을 진하게 출력하면 된다.

그래서 파일 열기 대화상자에서 파일이나 디렉터리를 우클릭하면, 탐색기에서 파일/디렉터리를 우클릭했을 때는 없던 '선택/Select'라는 명령이 추가되고 이것이 진하게 표시된 것을 확인할 수 있다.
또한 똑같이 창의 시스템 명령을 꺼냈더라도 아이콘을 클릭했을 때는 '닫기'가 진하게 나오고, 제목 표시줄을 우클릭했다면 '최대화' 명령이 진하게 나오는 것을 알 수 있다. 창의 해당 부위를 더블 클릭하면 실제로 그 동작이 행해지기 때문이다. 우클릭 메뉴의 default 아이템에는 이런 의미가 있는 것이다.

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

그런데 운영체제의 이런 동작에는 버그가 있다.
최대화된 프로그램의 제목 표시줄을 우클릭하면 '이전 크기로'가 진하게 나와야 하고, 최대화되지 않은 프로그램의 제목 표시줄을 우클릭하면 '최대화'가 진하게 나와야 한다.
그러나 탐색기, IE, MS 오피스 프로그램, Media Player 등, 마소에서 개발한 프로그램들은 그렇게 동작하지 않으며, 언제나 '닫기'만이 진하게 나온다. 시스템 메뉴를 직접 꺼냈을 때처럼 말이다.
그 반면, 메모장이나 <날개셋> 편집기 같은 프로그램은 바르게 동작한다. 어찌 된 일일까?

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

여러 프로그램들의 동작을 관찰해 보면, 차이는 단 하나뿐이라는 걸 알 수 있다.
바르게 동작하는 프로그램은 운영체제의 기본 메뉴를 사용하는 프로그램이다. 그 반면, 자체적으로 구현한 메뉴를 사용하느라 기본 메뉴를 사용하지 않는 프로그램은 제목 표시줄을 우클릭했을 때 default 아이템이 제대로 처리되지 않고 언제나 '닫기'만 진하게 나온다. 신기하지 않은가?

Windows라는 운영체제는 창을 생성할 때 또는 아예 창의 클래스를 등록할 때 메뉴 핸들을 같이 전달할 수도 있을 정도로 메뉴 처리를 각별히 신경 써서 만들어졌음에도 불구하고, 이젠 마소에서 자체 기본 메뉴를 구시대 물건으로 치부하고 매우 천대하고 있다. 이것은 어제오늘 트렌드가 아니다. 마소에서 직접 만드는 네임드급 프로그램들 중에 기본 메뉴를 사용하는 프로그램이라고는.. 거의 없다. 자체 메뉴를 쓰는 것도 모자라서 그걸 다 리본 UI로 대체하거나, 아니면 탐색기나 IE의 경우 Alt를 눌렀을 때에만 메뉴가 잠깐 나타난다. 그러니 제목 표시줄을 우클릭했을 때 '최대화'가 진하게 나오는 광경을 보기가 더욱 힘들어지고 있는 것이다.

게다가 본인이 확인한 바로는 이건 거의 Windows 98때부터 동일하게 이러고 있다. 크기 조절과 최소화· 최대화가 가능한 프로그램 윈도우치고 어떤 형태로든 메뉴가 없는 창은 보기가 힘들다. 그러니 이런 버그가 상대적으로 눈에 잘 안 띄었던 것인지도 모르겠다. Windows 95때는 컴의 성능도 열악하고, 그 시절에는 마소에서 만든 프로그램들도 거의 다 기본 보급 메뉴를 썼기 때문에 확인이 쉽지 않다.
아무튼 이거 굉장히 흥미로운 현상이다. 구닥다리 레거시 코드에 존재하고 딱히 심각한 문제도 아니니, 앞으로도 안 고쳐질지도 모르겠다.

Posted by 사무엘

2015/06/15 19:24 2015/06/15 19:24
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1105

요즘은 거의 찾을 수 없는 관행인데, 옛날에 메뉴가 달린 Windows용 프로그램 중에는 파일, 편집 같은 다른 메뉴는 왼쪽에 있는 반면 '도움말' 같은 마지막 메뉴 하나만은 맨 오른쪽에 따로 떨어져서 배치된 것들이 종종 있었다. 그게 유행이었다. 특히 16비트 Windows 3.x 시절에 말이다.

사용자 삽입 이미지

음성학 연구용으로 많이 사용되는 사운드 편집기 프로그램인 프라트(Praat)는 최신 버전까지도 그런 형태라는 게 흥미로웠다. 단어 배치로 치면 단순히 space가 아니라 왼쪽 정렬 탭과 오른쪽 정렬 탭으로 구분된 셈이다.

사족을 덧붙이자면, 얘는 도움말 메뉴뿐만이 아니라 프로그램 외형이 전반적으로 좀 범상치 않아서 혹시 qt나 자바 같은 범용 프레임워크로 GUI를 만들었나 하는 생각이 들었다. 허나 Spy++로 들여다보니 그렇지는 않아 보인다. 다만 컴파일러는 Visual C++이 아니라 gcc 계열을 써서 빌드 됐더라. 그리고 얘 자체가 크로스 플랫폼 프로그램이기도 하고 도움말까지 자체 구현인 걸 보면, 독자 개발한 자체 GUI 라이브러리 자체는 사용한 것으로 보인다. (그리고 이 정도 엄청난 프로그램이 무료 공개라는 게 참 대단하다!)

그나저나, 오른쪽에 따로 떨어진 메뉴 아이템은 어떻게 구현된 걸까?
본인은 아무 근거 없이 정말 막연하게.. 왼쪽의 맨 마지막 아이템과 오른쪽으로 밀려난 첫 아이템 사이에 "아마 separator 아이템이 있는 게 아닐까?"라고 오랫동안 생각했다. 하지만 실제로 이걸 넣어 보니, 아이템과 아이템 사이에 공간만 더 생길 뿐 정렬 방식이 달라지지는 않았다.

이걸 구현한 방식은 허무할 정도로 간단하다. 바로 체크/disabled 등을 나타내는 그 상태 플래그에 MFT_RIGHTJUSTIFY라는 정보도 같이 들어있다. 즉, 플래그에는 자신의 상태도 들어있고 속성도 같이 들어있는 것이다. 그건 뭐 윈도우 스타일도 마찬가지이지만.

여러 메뉴 아이템에 그 스타일이 있으면, 스타일이 등장하는 첫 아이템부터 나머지 메뉴 아이템들은 죄다 오른쪽에 정렬되어 나온다. 가로로 배치된 메뉴 말고 세로로 배치된 메뉴 내지 우클릭 팝업 메뉴 같은 데서는 이 플래그는 아무 기능도 하지 않는 잉여이다. 그걸로 끝이다.
마치 콤보 박스의 extended UI 스타일만큼이나 자주 보기 쉽지 않은 UI이다 보니, 더 특별한 방법이 있는가 싶었는데 다소 실망스럽기까지 했다.

예전에 메뉴에 대해서 한번 글을 쓴 적이 있었는데 그 당시에는 오른쪽 정렬 메뉴 아이템에 대해서는 미처 생각을 못 하고 있었다. 그러니 이번에는 메뉴와 관련해서 non-client 영역 얘기나 좀 더 하고 글을 맺겠다.
메뉴가 표시되는 영역다 응용 프로그램이 자체적으로 뭔가 출력을 하는 예로 옛날에 Freecell 게임이 있었다. 남은 카드의 수가 메뉴의 오른쪽 끝에 나타났기 때문이다.

사용자 삽입 이미지

프리셀은 유사품인 카드놀이(solitaire)와는 달리, 처음부터 32비트 코드 기반으로 개발되어 옛날에 Win32s와 함께 제공되기도 했던 역사적인 유물이다.

그런데 Windows XP로 오면서 살짝 옥에티가 생겼다.
XP의 기본 luna 테마에서는 가로로 배치된 메뉴 표시줄은 회색이다. 하지만 펼쳐진 메뉴 창은 흰색이다. 고전 테마 때는 이런 일이 없었는데 역사상 처음으로 메뉴 표시줄의 배경색과 메뉴 창의 배경색이 서로 달라진 것이다.
허나 프리셀은 "남은 카드 수"를 메뉴 창의 배경색으로 출력하는지 옅은 회색 배경에 흰색 배경으로 글자가 찍혀서 뭔가 이질감이 생겨 있다.

그래서 Vista인가 7부터 프리셀은 화면 하단에 상태 표시줄이 별도로 추가되었고, 남은 카드 수는 거기에다 출력하게 동작이 바뀌었다.

그림을 그리라고 운영체제가 보장을 해 준 클라이언트 영역 말고, 창의 프레임이나 제목 표시줄, 메뉴 표시줄 등은 논클라이언트(non-client) 영역으로 분류된다.
여기는 일단은 운영체제가 알아서 모든 처리를 해 준다. 그릴 일이 있을 때 WM_NCPAINT라는 메시지를 날려 주기는 하지만, 어지간해서는 응용 프로그램이 그걸 건드리지는 않는 게 좋다.

전에도 한번 말했듯이 MS Office는 95 시절에 캡션 바(제목 표시줄)를 독자적으로 그러데이션을 입혀서 그리곤 했다. 이건 1회 유행으로 끝났고 그러데이션은 나중에 Windows 98에서 완전히 전체적으로 적용되었다.
더 옛날 16비트 시절에 시스템 차원의 훅킹이 더 쉽던 시절엔 모든 창의 캡션에 응용 프로그램이 자신의 기능을 수행하는 버튼 같은 것도 막 집어넣기도 했던 것 같다.

허나, 그 동작을 어설프게 가로채면, Windows가 버전업 되어서 논클라이언트 영역의 비주얼이 또 바뀌었는데 응용 프로그램은 동기화가 안 되어서 외형이 이상해지고 프로그램이 오동작 할 수가 있게 된다.
일례로, 과거에 아래아한글 97은 논클라이언트까지 완전히 독자적으로 GUI를 그리던 대표적인 프로그램이다. 그런데 Windows XP 테마에서는 윈도우의 논클라이언트 가장자리에 둥그런 모서리 region이 적용되었다.
하지만 아래아한글 97은 기본 region은 딱히 건드리지 않고 그림만 직사각형 region을 기준으로 곧이곧대로 그렸기 때문에 가장자리가 짤려서 대화상자에 약간 glitch가 있었던 것이다.

하지만 Windows는 맥 OS처럼 GUI가 선택의 여지가 없이 독재 획일화인 운영체제는 아닌지라 당장 MS 자신들부터가 Office나 Visual Studio 같은 중요한 밥줄 프로그램들은 운영체제의 표준 GUI 따위는 전혀 안 쓴다.
특히 리본 UI는 논클라이언트를 완전히 제멋대로 재정의해서 쓴다. 논클라이언트 영역에 적용되는 Aero 투명 효과까지 세밀하게 제어하면서 말이다. 지금은 Aero가 폐기돼서 별 의미는 없어졌지만 말이다.

MS 같은 GUI 잉여짓을 할 여력이 없는 프로그래머라면 WM_NCPAINT를 다룰 일은 별로 없겠지만, custom 컨트롤을 만드는 경우라면 불가피하게 이 메시지를 직접 처리해야 하게 된다.
공용 컨트롤 6 매니페스트가 적용되었더라도 윈도우의 테두리는 그냥 가만히 놔 두면 새끈한 테마 형태가 아니라 옛날의 밋밋한 기본 스타일로 그려지기 때문이다. OpenThemeData와 DrawThemeBackgroundEx 같은 함수로 수동으로 그려야 한다.

이런 부류의 글은 결론이 언제나 동일하다. Windows 프로그래밍은 재미있더라.

Posted by 사무엘

2015/05/16 08:25 2015/05/16 08:25
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1094

DllMain 이야기

Windows 운영체제에 실행 바이너리로는 잘 알다시피 EXE와 DLL이 있다.
EXE에 시작 지점이 WinMain이 있듯, DLL에는 DllMain이라는 시작 지점이 있어서 간단한 자신의 시작과 종료 같은 이벤트 통지를 받아서 초기화/마무리 작업을 할 수 있다. 이건 static library에는 없는 개념이다.

이 함수는 export table의 이름이나 ordinal로 탐색하는 게 아니라, PE 실행 파일 헤더 차원에서 주어져 있는 entry point의 주소로 진입하는 것이기 때문에 이름이 딱히 export되어 있지 않아도 된다.
물론 요즘 DLL은 코드 셔틀뿐만 아니라 리소스 셔틀의 역할도 많이 하기 때문에(특히 다국어 지원용!) 데이터만 추출한다는 플래그를 주고 LoadLibraryEx를 호출했거나 DLL 자체가 리소스 전용으로만 만들어졌다면 그런 DLL은 DllMain 함수가 없거나, 있더라도  실행되지 않는다.

실행 파일은 자신이 말 그대로 실행 주체이기 때문에 WinMain 함수의 리턴이 곧 해당 프로세스의 종료를 뜻한다. 그러나 DLL은 어떤 함수 호출이 있을 때에만 그때 그때 실행되는 형태이기 때문에 DllMain도 이벤트만 받고는 금세 리턴된다는 차이가 있다. 즉, 이런 점에서 DllMain은 WinMain보다는 윈도우 프로시저와 더 비슷한 형태이다.

DLL이라는 단어 자체가 대문자 이니셜이긴 하지만, 일단 C/C++ 컴파일러가 링크 때 참조하는 명칭은 DLLMain이 아니라 DllMain이다. L은 소문자로 쓰니 스펠링을 혼동하지 않도록 주의하자.
함수의 인자로는 자기 자신의 인스턴스(모듈) 핸들, 그리고 호출 이벤트(DWORD fdwReason)와 부가 인자(PVOID)가 들어오며, 리턴값은 BOOL이다.

이벤트는 4가지 종류가 있다.
일단, DLL이 처음 로딩되어 어떤 프로세스에 붙었을 때(DLL_PROCESS_ATTACH), 그리고 반대로 종료될 때(*_DETACH) 두 가지 경우가 당연히 포함된다. 이것은 LoadLibrary 내지 FreeLibrary 같은 런타임 동작으로 인한 결과일 수도 있고, 아니면 실행 파일의 import 테이블 차원에서 로딩되거나 해당 프로세스가 종료되어 딸린 DLL들이 모두 일괄 종료되는 경우일 수도 있다.

그 다음으로 DLL들은 특별히 그 프로세스 안에서 스레드가 새로 생성되거나 종료되었을 때도 통지를 꼬박꼬박 받는다. DLL_THREAD_ATTACH와 *_DETACH. 이건 응당 16비트 시절에는 없다가 나중에 새로 생긴 정보일 것이다. 그리고 당연한 말이지만 그 생성되거나 소멸되는 스레드의 실행 문맥으로 함수가 호출되므로 GetCurrentThreadId 같은 함수로 지금 스레드 ID 따위를 쉽게 조회해 볼 수 있다.

다만, 이 통지는 이 DLL이 로딩되기 전부터 이미 존재하던 스레드에 대해서는 오지 않으며, 스레드가 하나씩 차례대로 종료되는 게 아니라 메인 스레드가 실행이 끝나서 프로세스의 스레드들이 죄다 한꺼번에 종료될 때는 스레드별로 detach 통지가 오지 않는다. 그러므로 실행 중인 모든 스레드를 한데 조회하는 건 다른 API를 써서 해야 하며, 스레드별로 만들어 뒀던 리소스를 한꺼번에 해제하는 건--가령, 스레드별로 TLS 슬롯들이 가리키는 추가 할당 메모리-- *_PROCESS_DETACH 에다가도 마련해 둬야 한다.

DllMain의 추가 인자는 사실상 *_PROCESS_DETACH일 때에만 쓰인다.
이 DLL이 FreeLibrary로 인해서 레퍼런스 카운트가 0으로 떨어져서 동적으로 해제되는 것이면 추가 인자에는 0이 들어오고, 그렇지 않고 호스트 프로세스가 종료되면서 자명한 이유로 인해 같이 해제되는 것이면 1이 들어온다.
그렇기 때문에 *_PROECSS_DETACH일 때 어떤 값이 들어오느냐에 따라 혹시 모듈의 reference count leak가 있지는 않았는지를 체크할 수 있다.

운영체제가 기본 제공하는 시스템 DLL을 쓰는 게 아닌 이상, 내 EXE에서 쓰는 내 custom DLL은 대부분 동적으로 읽어들이고 해제하는 게 일반적이다. (delay-load 포함) 그런데 한번 읽었던 동일 DLL에 대해 자꾸 LoadLibrary를 반복 호출하면 해당 DLL의 레퍼런스 카운트가 증가하기 때문에 나중에 FreeLibrary를 한 번만 했을 땐 메모리에서 해제가 되지 않게 된다. 그 즉시 DLL_PROCESS_DETACH에 0이 들어오면서 DLL이 해제가 되지 않는다면 어딘가에 버그가 있다는 뜻이다.
결국 프로세스가 종료될 때가 돼서야 궁극적으로 해제가 되긴 하지만(이때는 1이 들어옴), 어쨌든 이런 것도 넓은 의미에서는 memory leak이 되는 셈이다. COM 오브젝트의 레퍼런스 카운트 관리와 완전히 똑같은 문제다.

*_PROCESS_ATTACH일 때에도 추가 인자에는 LoadLibrary에 의한 동적 로드일 때는 0, 그렇지 않고 import 테이블에 의한 정적 로드일 때는 1이나 이에 준하는 nonzero 값이 온다고는 하는데 본인은 그건 확인을 못 해 봤다.
그리고 DLL의 입장에서는 자신이 어떤 방식으로 로드되느냐에 따라 딱히 달리 동작하거나 자신의 로딩 방식을 굳이 알아야 할 일은 거의 없다.
이것 말고 단순히 스레드 생성/소멸 통지 때는 추가 인자는 쓰이지 않고 그냥 0만 온다.

한편, DllMain 함수의 리턴값은 일반적으로는 쓰이지 않고 1을 되돌리든 0을 되돌리든 무시된다.
이게 쓰이는 단 한 가지 상황은 *_PROCESS_ATTACH 때로, 이때는 함수가 TRUE를 되돌려야 DLL의 로딩이 성공한 것으로 간주된다. 안 그러면 이 DLL의 로딩은 거부되며 LoadLibrary의 리턴값은 NULL이 된다.

과거에 Visual C++이 2005와 2008 시절에 CRT와 MFC 라이브러리에 대해서 side-by-side assembly 방식을 강제 적용해서 이 방식으로 DLL이 로딩되지 않으면 로딩을 거부하곤 했는데, 그것 판단을 DllMain 함수에서 했다. 즉, 자신이 단순히 EXE와 같은 디렉터리나 운영체제 시스템 디렉터리에 있어서 로딩이 된 것이라면 DllMain이 고의로 FALSE를 되돌렸던 것이다.

그리고 중요한 점으로는.. DllMain의 *_PROCESS_* 실행 시점은 C++ 프로그램으로 비유하자면 main 함수가 실행되기도 전에 전역 클래스 객체의 생성자 내지 소멸자 함수가 실행된 것과 같다.
범용적인 DLL는 정말 기상천외한 EXE에 붙을 수도 있으며 DllMain의 호출 시점은 주변의 다른 DLL들이 모두 제대로 로딩 됐다고 장담할 수가 없는 때이다. 그렇기 때문에 이때는 다른 복잡한 초기화를 하지 말고 가능한 한 우리의 영원한 친구인 kernel32에 있는 함수만 호출해야 한다.

DllMain에서 할 만한 좋은 작업의 예로는 TLS 슬롯 할당, heap, 뮤텍스, 크리티컬 섹션, memory mapped file 같은 커널 오브젝트의 간단한 초기화 정도이다. 그 외에 user32나 gdi32까지 가는 작업은 권장되지 않으며 하물며 레지스트리나 COM/OLE 같은 시스템을 건드리는 정도만 되면 절대 금지이다.

이 함수 안에서 또 다른 DLL을 연쇄적으로 불러들이거나 해제하는 작업도 금물이다. 그게 가능하다면 참 편할 것 같지만 운영체제의 입장에서는 예측할 수 없는 엔트로피를 키우는 일이며 데드락을 야기할 수 있다. (특히 상호간에 LoadLibrary를 하는 경우는..?? =_=) 완전히 같은 예는 아니지만 생성자 안에서 가상 함수를 호출하는 게 왜 금지되어 있는지를 생각해 보자.

그러니 더 복잡하고 정교한 초기화나 마무리 작업은 DllMain에서 하지 말고, 함수를 따로 만든 뒤에 이 DLL의 사용자로 하여금 그걸 별도로 호출하게 해야 한다.
윈도우 컨트롤을 제공하는 DLL이라면 그냥 LoadLibrary를 하는 순간에 컨트롤의 윈도우 클래스들이 자동으로 등록돼 버리면 좋겠지만, 일단은 윈도우 클래스도 user 계층 관할이기 때문에 초기화를 별도의 함수에서 하는 게 바람직하다.

CWinApp 개체가 존재하는 MFC 확장 DLL의 경우, InitInstance와 ExitInstance가 호출되는 타이밍이 역시 DllMain이다. 그 때밖에 기회가 없으니 어찌 보면 당연한 얘기이다. 그러니 그때에도 사용하는 함수에 동일한 제약을 적용하여 주의해야 한다. 이건 Lyn 님의 블로그에서 발견한 정보임을 밝힌다. ^^

끝으로, 스레드와 관련하여 하나만 더 첨언하고 글을 맺겠다.
*_PROCESS_* 메시지야 어느 DLL에게나 자신의 생명 주기를 알리는 필수불가결한 메시지이겠지만, *_THREAD_*의 경우는 그렇지 않다. 모든 DLL들이 스레드 생성이나 소멸을 일일이 통보 받아야 할 필요는 없다.
그렇기 때문에 스레드 통보를 받을 필요가 없는 DLL은 DisableThreadLibraryCalls라는 함수를 호출함으로써 *_THREAD_*를 받지 않겠다고 운영체제에다 알려 주는 게 조금이나마 성능 향상에 도움이 된다.

얘는 물론 DllMain에서 곧장 호출해도 안전한 kernel32 함수이다. 수시로 켰다 껐다 할 필요가 있는 옵션이 아니어서 그런지, 한번 지정만 하고는 끝이다.
개인적으로는 이런 간단한 정보는 DllMain + DLL_PROCESS_ATTACH의 리턴값 플래그로 접수하는 게 좋지, 굳이 별도의 함수로 만들 필요가 있었나 싶은 생각이 든다. 리턴값이 0이면 로드 거부, 1이면 로드 허용, 2이면 로드 허용하되 앞으로 이 DLL은 스레드 통지는 안 함 정도로. 하지만 함수가 저렇게 만들어져 버렸으니 프로그래머의 입장에서는 그걸 적절히 사용만 하면 되겠다.

다만 C 라이브러리를 static 링크하는 DLL은 저 함수를 사용하지 않는 게 좋다. 자기는 스레드 통지를 사용하지 않더라도 배후의 CRT가 스레드 통지를 내부적으로 활용하기 때문이다. 물론 CRT를 DLL 링크하는 DLL에 대해서는 이런 제약이 적용되지 않는다.
그렇기 때문에 한 소스에 대해 CRT를 static/DLL 여러 방식으로 빌드하는 DLL이라면 #ifdef _DLL에 따라서 DisableThreadLibraryCalls 또는 __noop으로 대응하는 매크로 함수를 만들어 사용하는 게 바람직하다.

Posted by 사무엘

2015/04/28 08:20 2015/04/28 08:20
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1088

이건 예전에 썼던 disabled 윈도우 관련 글에서 추가로 다뤘어야 했는데 그때 빠뜨렸던 내용이다.
그 글에서 설명했듯, Windows는 '고전 테마'에서 메뉴, 버튼, static 컨트롤이 disable된 형태는 그냥 회색으로만 표시하는 게 아니라 흰색 윤곽 위에다가 회색 윤곽을 덧씌워서 일종의 '엠보싱' 효과를 줘서 표시한다.

사용자 삽입 이미지

이것들은 은색(밝은 회색, 일명 3D 배경색)이 배경이라는 공통점이 있다. 시스템 컬러 번호는 COLOR_3DFACE. 사용자가 딱히 변경을 할 수 없는 고정된 문자들이다.

그 반면, 리스트 박스나 에디트 컨트롤처럼 흰 배경(COLOR_WINDOW)에 표시되는 문자들은 사용자의 선택이나 입력에 의해 내용이 바뀔 수가 있다. 이런 것은 disable됐을 때 배경이 회색으로 변하고 문자는 딱히 엠보싱 처리가 되지 않는다. 각각의 경우에 비주얼이 나름 원칙이 있게 설계된 셈이다.

그런데 먼 옛날에 Windows 9x 시절에 안전 모드 부팅을 해 본 분들은 뭔가 이상한 낌새를 느끼지 않으셨나 모르겠다.
안전 모드에서는 일체의 외부 디바이스 드라이버를 불러들이지 않는 관계로 그래픽조차도 완전 미개한 VGA 640*480 16색으로로 돌아가 버린다.

회색과 파랑의 기본색 팔레트가 다른 톤으로 바뀌는데, 이건 그렇다 치더라도
안전 모드에서는 disable UI가 엠보싱이 아니라 그냥 짙은 회색 단색으로 간소화되어 출력된다. 그래서 느낌이 더욱 달라진다.

사용자 삽입 이미지

엠보싱을 표현하는 데는 흰색과 회색 단 두 색만 있으면 된다. 색깔 표현의 한계 때문에 엠보싱을 포기한 건 결코 아니다.
결정적으로 NT 커널 기반인 Windows 2000은 똑같이 VGA 16색인 안전 모드에서도 동일하게 엠보싱 처리를 해 준다. 어찌 된 일일까?

사용자 삽입 이미지

the old new thing 블로그에 관련 설명이 있다.
시스템 정보를 얻는 API 함수 중 하나인 GetSystemMetrics를 보면 SM_SLOWMACHINE이라는 아이템이 있는데,
얘의 리턴값이 true일 정도로 열악한 환경에서는 운영체제 셸은 disable UI에 엠보싱을 포기하고 그냥 회색 단색을 출력한다고 한다.

이 플래그는 컴이 486이 안 되고 램이 6MB도 안 되는.. 그야말로 윈도 3.1만 간신히 돌리던 극도의 똥컴에서나 켜지는 정도였다. 윈도 95가 당시 사용자의 컴퓨터 환경을 감안하여 명목상으로는 386+램 4MB 이상을 기준으로 설계되었다는 걸 생각해 보자. 물론 한글판은 한글 입출력 오버헤드 때문에 그런 사양에서는 어림도 없으며, 최하 486에 램 8MB 이상은 기본으로 갖춰져야 했다.

그리고 컴퓨터가 아니면 그래픽 카드가 완전 구릴 때에도 이 플래그가 설정되었다.
이를테면 마우스 포인터 깜빡임 보정조차 안 될 정도로 안습일 때 말이다. 기본 VGA는 하드웨어 가속이고 뭐고 아무것도 없는 느려터진 모드이다. 당장 시스템 종료를 위해 Alt+F4를 눌러 보면, 화면 배경 전체에 검은 도트가 반씩 씌워지는 것조차도 단번에 안 되어 점이 내려오는 게 보일 지경이다.

안전 모드에서 disable UI가 엠보싱 없이 출력되었던 것은 바로 그래픽 모드 때문이었다.
엠보싱은 지금 컴퓨터의 관점에서야 그야말로 껌값인 처리이지만, 나름 더블 버퍼링이라는 오버헤드가 필요한 연산이었으니 말이다.

이런 9x와는 달리, NT 계열은 1993년에 출시된 첫 버전 3.1부터가, 그것도 한글· 한자 오버헤드 따위도 없는 영문 원판이 램이 최하 12MB 이상 필요한 왕창 무거운 물건이었다. 범용성과 안정성, 이식성을 위해서 컴의 성능을 쫙쫙 빼다 쓰는 형태로 설계되었기 때문이다. 기본 문자 집합부터가 1바이트가 아닌 2바이트 크기였으니 그것도 메모리를 추가로 잡아먹었을 테고. (그러니 그 시절에 NT를 돌릴 수 있는 컴을 가진 사람이 도대체 얼마나 됐겠는가.)

블로그 글에 따르면, NT는 그야말로 “All machines are fast.”라고 가정하고 태생적으로 SM_SLOWMACHINE 플래그를 사용하지 않는다고 한다. 즉, 어떤 컴퓨터에서나 언제나 false를 되돌린다.
그러니 Windows 2000에서는 VGA 16색 안전 모드에서도 UI에 엠보싱이 적용되는 이유가 논리적으로 바로 설명이 된다.

단, 신기한 것은 2000은 VGA 16색 안전 모드에서도 그래픽이 그렇게 느리게 느껴지지 않는다는 점이다.
게다가 그 모드에서도 그래픽이 마구 바뀌는 곳에 마우스 포인터를 가져갔을 때 포인터가 깜빡거리지 않는다! 하드웨어 제어를 어떻게 했는지 굉장히 궁금해지는 대목이다. 뭔가 굉장히 탄탄하고 안정적이라는 느낌이 든다.

Windows XP부터는 더 나아가 이제 안전 모드에서도 VBE인지 뭔지 슈퍼 VGA 규격을 사용한다. 비록 하드웨어 가속이 없을지언정 일단 트루컬러는 무조건 보장된다. 그래서 초라한 16색, 256색 따위는 정말로 볼 일이 없어졌으니 참 격세지감이다. 그냥 16비트 컬러냐 32비트 컬러냐의 양자 선택만이 있을 뿐이다.

그리고 고전 테마 말고 새로운 테마에서는 disabled UI에 엠보싱 자체를 하지 않는다. 오히려 SM_SLOWMACHINE 스타일과 같은 맥락인 회색 단색으로 회귀했다. 고전 테마가 아예 없어진 Windows 8부터는 엠보싱은 아련한 과거 추억이 됐다.

그래픽 모드가 아예 단색일 때는 diabled UI는 글자에다가 배경색 도트를 반반씩 뿌려서 흐리게 했었는데 그게 16컬러 시절에 엠보싱으로 바뀌었고, 이것이 궁극적으로는 알파 채널로 변모하는 듯하다. 사실 엠보싱은 트루컬러+알파채널+ClearType에 친화적인 방식이 아니긴 하니까 말이다. (맑은 고딕을 엠보싱해서 흐리게 출력하면 보기가 대략 좋지 않다)

참고로 MS Office 제품 중에 Word와 Excel은 운영체제의 대화상자 API가 아니라 자체 개발한 대화상자와 GUI 라이브러리를 사용한다. 얘들은 고전 테마 기준으로 push 버튼은 엠보싱으로 출력하지만, 라디오나 체크 박스는 단순 회색으로 disabled 상태를 출력한다. 즉, 비주얼이 짬뽕이며, 운영체제 GUI와 외형이 완전히 같지는 않다.

사용자 삽입 이미지

끝으로..
Windows에 사용자의 컴퓨터 성능을 체크하는 듯한 기능 몇 군데를 좀 살펴보고 글을 맺도록 하겠다.
하나는 비스타와 7 시절에 있던 그 유명한 Windows 체험지수인데, 이건 8부터는 없어졌다.
다른 하나는 '내 컴퓨터' 속성의 "고급 - 성능 옵션"에서 각종 시각 효과를 설정하는 곳이다.

이건 Windows XP에서 처음 도입된 걸로 기억하는데, 윈2000이나 돌릴 법한 좀 간당간당한 컴에서 "최적 성능으로 조정"을 켜면 그림자 효과나 창의 애니메이션 등 몇몇 효과가 알아서 제외된다.
하지만 이것은 SM_SLOWMACHINE 플래그와는 별개로 구현된 기능이라고 생각하면 되겠다. 또한, 요즘 컴퓨터에서는 그런 거 성능이 문제되는 경우는 전혀 없다고 봐도 무관하고.

아무튼 흥미로운 사실을 하나 알게 됐다.

Posted by 사무엘

2015/04/11 08:30 2015/04/11 08:30
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1082

Windows API 메모

1.
Windows API에서 DrawText는 gdi도 아니고 user 계층에 있는 고급 함수인 주제에 여러 줄(DT_SINGLELINE 플래그 없는 기본 모드)을 찍을 때에도 세로 정렬(DT_VCENTER, DT_BOTTOM)을 좀 지원해 주면 어디 덧나나 싶다. 오래 전부터 개인적으로 매우 대단히 아쉽다고 생각해 온 점이다.

얘는 gdi 계층에 있는 다른 글자 출력 함수들과는 달리, 글자수를 -1 (null-terminate string을 가정하고 알아서 길이를 계산하게)로 줄 수가 있으며, 긴 파일/디렉터리 이름의 중간을 생략하여 찍거나 액셀러레이터 &를 다음 글자의 밑줄로 바꿔 출력하는 기능, 심지어 밑줄만 출력하는 기능도 있다.
& 전처리의 경우, 하는 게 아니라 “끄는 게” 별도의 플래그로 주어져 있을 정도로 기본 기능이다.

그러니 이건 천상 운영체제 내부에서 자기네 GUI 출력용으로 쓰는 함수인데 제3자도 사용할 수 있게 공용 API로 열어 놨다는 뜻이다.
안 그래도 텍스트를 처음부터 끝까지 쭉 읽어 봐야만 할 수 있는 처리들이 즐비한데, 그에 비해 멀티라인 텍스트의 세로 정렬은 텍스트 전체에서 \n 개수를 세어서 줄 수만 파악하고 나면 아주 손쉽게 구현 가능한 처리이다.
그러니 왜 지원을 안 하는지가 몹시 의문이다.

공교롭게도 운영체제의 컨트롤들 중에 static text는 DrawText의 기능을 사용해서 그런지 multiline 상태에서 세로로 중앙이나 아래 정렬을 하는 옵션이 없다.
그러나 버튼(push, radio, check 모두)들은 그런 옵션이 있다.

2.
아마 이건 예전에 의견을 한번 피력한 적이 있는데 다시 적자면..
본인은 선에 안티앨리어싱을 해서 그리는 기능 정도는 그냥 Pen 관련 GDI 함수/구조체에다가도 스타일로 추가해서 지원을 좀 해 줬으면 하는 생각을 한다. PS_SMOOTH 정도로..;;
마치 Cleartype이 적용된 글자를 찍기 위해 생소한 API를 굳이 사용할 필요가 없는 것처럼 말이다. 그냥기존 LOGFONT 구조체의 lfQuality에 새로운 값이 추가되는 걸로 훌륭하게 잘 구현되지 않았던가.

21세기 초에 야심차게 도입됐던 GDI+는 하드웨어 가속 버프도 없이 거의 버림받은 신세가 됐고, Direct2D는 COM을 사용하는 등 API 패러다임이 너무 다르다.
하지만 GDI는 유구한 역사를 자랑하는 Windows의 창립 멤버 API이고 이제 와서 도저히 버릴 수가 없는 압도적인 짬밥을 보유하고 있으니.. 그냥 유지보수 차원에서만 지원되는 legacy가 돼 버렸고 GDI API에 근본적인 확장은 없을 것으로 생각된다.

3.
유니코드 UTF16 문자열과 여타 8바이트 기반 인코딩(UTF8 포함) 사이를 변환하는 API 함수는 잘 알다시피 WideCharToMultiByte와 MultiByteToWideChar이다.
얘는 Windows NT가 유니코드+2바이트 wide char 기반으로 통 크게 설계되었을 때부터 역사를 함께 해 왔다. 옛날에 Windows 3.x에다가 Win32s를 설치하면 단순히 32비트 커널+썽킹 코드뿐만 아니라 코드 페이지 변환 테이블도 잔뜩 설치되었다. 32비트 EXE/DLL은 리소스의 내부 포맷부터가 유니코드인 관계로, 이들을 당장 변환할 수 있어야 하기 때문이다.

유니코드에서 여타 인코딩으로 변환하는 것은 마치 double에서 short로의 형변환처럼 큰 집합에서 작은 집합으로 이동하는 변환이다. 그러니 인코딩에 존재하지 않는 문자는 ? 같은 default 문자로 치환된다.
그런데, 별도의 플래그가 없다면 WideChar... 함수는 약간의 '유도리'를 발휘하여 동작한다. 여러 유니코드 문자가 한 여타 인코딩으로 변환될 수 있다는 뜻이다.

예를 들어, 유니코드를 KS X 1001로 변환한다고 치면, 원래 거기에 있던 호환용 한글 자모 ㄱ(U+3131)만 0xA4, 0xA1로 바꾸는 게 아니라 표준 한글 자모 영역에 있는 U+1100(초성 ㄱ)과 U+11A8(종성 ㄱ)까지 다 호환용 한글 자모 ㄱ으로 바꾼다는 뜻이다. ?로 바꾸지 않는다.
이런 예가 호환용 한글 자모나 일부 유럽 문자에 대해서 더 존재한다. 유럽 문자라 함은, 대문자 버전이 존재하지 않을 경우 그냥 소문자 버전으로 바꾸는 식이다.

이런 동작을 원하지 않고 엄밀하게 변환을 하고 싶다면 WC_NO_BEST_FIT_CHARS라는 플래그를 반드시 줘야 한다. 얘는 변환된 타 인코딩을 유니코드로 역변환했을 때 원래의 유니코드로 정보가 유지되지 않는다면 무조건 ?로 바꾼다. 즉, U+11??대의 표준 한글 자모는 호환용 한글 자모로 바뀌지 않는다. 이 옵션은 Windows NT4에도 존재하지 않으며, 98/2000부터 새로 추가된 얼마 안 되는 기능이다.

어느 방식을 사용할지는 그야말로 상황에 따라 다르다. 문자열을 복사하는 함수만 해도 버퍼 크기가 초과되었을 때 그냥 뒷부분을 융통성 있게 잘라 버려도 괜찮은 경우가 있는가 하면, 반드시 정확도가 보장되어야 해서 차라리 예외가 발생해야 하는 경우도 있을 수 있으니 말이다.

한편, 여타 인코딩에서 유니코드로 바꾸는 경우는 작은 집합에서 큰 집합으로 가는 것이니 일단은 유니코드에 대응하지 못하는 문자 걱정은 없다.
하지만 아무래도 여러 바이트가 한 글자를 구성하다 보니 정규화가 잘못되어서 해당 인코딩에 해당하지 않고 유니코드로 변환 자체가 될 수 없는 바이트 나열이 들어있을 수 있다. 이 경우는 유니코드로 변환했다가 다시 그 인코딩으로 역변환을 했을 때 바이트 나열이 원래대로 돌아올 수가 없게 된다.

이런 일이 발생했는지를 엄격하게 체크하려면 Multi... 함수에다 MB_ERR_INVALID_CHARS 플래그를 주면 된다.
<날개셋> 편집기는 이 두 경우를 모두 체크하여 불러오기가 제대로 되지 않았을 때, 혹은 저장과 함께 정보가 소실될 우려가 있을 때 경고 메시지가 나온다.
저장이야 UTF8 내지 UTF16 같은 유니코드 계열 인코딩만 골라 주면 문제가 없지만, 불러오기 자체가 문제가 있었다면 그 어떤 인코딩을 쓰더라도 다시 저장하는 순간 정보 소실이 생기기 때문이다.

4.
다음으로, 우클릭 메뉴를 구현할 때 즐겨 쓰이는 TrackPopupMenu(Ex) 함수에 대해서도 좀 한 마디 하겠다.
사실 얘는 굳이 임의의 지점을 우클릭했을 때 외에도, 어떤 버튼을 눌렀을 때 메뉴가 튀어나오게 하는 용도로도 많이 쓰인다. 그래서 Ex 버전에서는, 메뉴가 상하좌우 좀 치우친 곳에서 튀어나와서 위치 보정이 필요하더라도, 그 버튼 영역은 메뉴에 의해 가려지지 않게 하는 유용한 옵션이 추가되었다.

윈도 Vista 이상에서부터는 버튼의 오른쪽 끝에 ▼라는 split 버튼을 넣는 옵션이 추가된 관계로, 팝업 메뉴는 이 UI와 연동되어 즐겨 사용된다. 본인이 개발하는 <날개셋> 한글 입력기의 제어판 UI에도 물론 적극 활용되었다.

그런데 그건 그렇고.. 본인이 이 함수에 대해서 좀 이해가 안 되는 면모는 크게 두 가지이다.
얘는 HWND를 하나 인자로 받는다. 사용자가 메뉴를 ESC로 취소하지 않고 뭔가 항목을 선택하면 그 명령 ID가 부모 윈도우에다가 WM_COMMAND의 형태로 전달된다. 이것은 일단은 팝업 메뉴 말고도 단축키 내지 프로그램 창에 기본으로 딸린 메뉴를 선택했을 때와 동작의 일관성을 맞추기 위한 조치이다.

그러나 그렇게 하지 말고 사용자가 선택한 명령 ID가 그냥 함수의 리턴값으로 바로 오게 할 수도 있다. DLL 같은 걸 만들기 때문에 응용 프로그램의 기본 메뉴 연계 따위를 생각 안 하는 환경에서는 이런 디자인이 훨씬 더 유용하다. 그래서 이때는 flag에다가 TPM_RETURNCMD를 주면 된다.

사소해 보이는 팝업 메뉴의 디자인도 이렇게 두 양상으로 나누어 생각할 수가 있는 것이다.
마우스의 드래그 드롭 동작을 각 WM_LBUTTONDOWN, WM_MOUSE, WM_LBUTTONUP 핸들러 함수에다 제각기 따로 처리할지, 아니면 WM_LBUTTONDOWN 안에다가 또 message loop을 만들어서 한 함수 안에다가 다 집어넣을지의 차이와 비슷한 맥락이다.

아무튼, 메뉴에서 TPM_RETURNCMD에 대해, MSDN에는 "determine the user selection without having to set up a parent window for the menu."라는 문장까지 버젓이 있는데..
그럼에도 불구하고 TPM_RETURNCMD가 있더라도 HWND hParent의 값은 어떤 경우에도 NULL이어서는 안 된다. 심지어 자신이 만들지 않은 다른 윈도우(데스크톱 전체 윈도우 같은)를 줘도 안 되고 동작이 실패한다.

WM_COMMAND를 안 받으면 이 윈도우는 정말 레알 천하에 필요하지 않은데도 말이다. 애초에 메뉴가 튀어나오는 좌표도 언제나 화면 좌표이지 부모 윈도우 같은 걸 받지도 않는다. 그래도 이 윈도우는 없으면 안 된다.
그래서 <날개셋> 한글 입력기는 부득이하게 화면에 표시도 안 되는 message-only 윈도우를 간단히 만들어서 이걸 셔틀로 삼아 메뉴를 띄운 뒤, 메뉴가 사라지자마자 그 윈도우를 메시지 펌핑 하나 안 하고 파괴해 버리는 꼼수를 불가피하게 쓴 부분도 있다. 순전히 삽질이다.

이게 한 가지이고, 다른 하나는.. TPM_NONOTIFY라는 플래그는 왜 있느냐는 것이다. TPM_RETURNCMD 플래그가 있으면 명령 ID는 리턴값으로 오고 WM_COMMAND가 가지 않아서 이미 no notify의 효과가 나는데 저 플래그가 또 하는 일이 무엇인지 MSDN만 봐서는, 또 내 직관과 경험만으로는 모르겠다. 알 수 없는 노릇이다.

5.
인터넷에서 갓 다운로드한 파일은 운영체제가 뭔가 좀 다르게 취급한다는 걸 컴퓨터(일단은 Windows 기준으로) 사용자라면 경험적으로 다들 아실 것이다.
Word나 Excel 같은 프로그램에서 문서를 열면 "이 문서는 인터넷에서 가져온 것이기 때문에 위험할 수 있다. 매크로를 기본적으로 꺼 놨다" 이런 꼬리표가 붙는다. msi나 exe는 잠재적인 범죄자로 취급되며, 특히 디지털 서명 같은 게 없으면 다루기가 정말 까다로워져 있다.

먼 옛날 2000년대 중반엔 Windows XP에 보안 업데이트가 행해져서 이렇게 '인터넷 다운로드'로 분류돼 있는 CHM(컴파일된 HTML)은 아예 화면에 표시가 되지 않게 됐다. 파일 속성을 들어가서 '차단 해제'를 해 줘야만 이들 파일도 일반 파일들과 동등하게 다룰 수 있게 된다.
(XP도 초창기엔 읽기 전용 매체인 CD도 아니고 USB 메모리가 autorun.inf 실행이 됐을 정도로 UI 차원에서의 보안이 굉장히 막장이긴 했다. 이것도 다 훗날 보안 업데이트를 통해 막혔음.)

그나저나 저런 '다운로드 파일' 보안 속성은 운영체제 내부에서는 어떻게 구현되어 있을까?
가장 간단하게 생각할 수 있는 방법은 도스 시설부터 존재했던 파일 속성이다. 일명 ARHS(기록, 읽기 전용, 숨김, 시스템)의 형태로 존재하던 것 말이다. 실제로 Windows에는 이것 말고도 압축/암호화 등 내부적으로 쓰이는 속성이 더 있다.

하지만 다운로드 속성은 그런 비트 형태의 속성으로 구현되어 있지는 않다.
바로 파일 시스템 차원에서 제공되는 대체 데이터 스트림이 해당 파일에 꼬리표처럼 붙는데 거기에 있는 zone identifier가 이 파일이 인터넷에서 왔음을 나타낸다.

대체 데이터 스트림은 당장 내 컴퓨터의 다운로드 디렉터리에서 DIR /r을 하면 정체를 확인할 수 있다. 내 기억이 맞다면 CreateFile 함수로 저 대체 스트림의 내용을 바로 확인할 수도 있으며, IZoneIdentifier 인터페이스 등을 얻어서 이것을 조작할 수도 있다. 물론 저 꼬리표를 제거하는 것도 포함해서 말이다. 자세한 방법 소개는 The Old New Thing 블로그 내용을 링크하는 것으로 대체하겠다.

이런 기능이 과거의 FAT 계열 파일 시스템에서 가능했을 것 같지는 않고.. 언제 도입되었는지는 잘 모르겠다. 최소한 Windows 9x 시절의 IE 6 미만에는 없었던 것 같다.

Posted by 사무엘

2015/04/05 08:35 2015/04/05 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1080

* 지금으로부터 무려 4년 전에 Windows 공용 컨트롤에 대해서 글을 쓴 적이 있었는데 오늘은 그에 대한 연장선이다. 또 옛날 이야기를 늘어놓아 보겠다.

예전에도 글을 썼듯이, 공용 컨트롤은 좀 더 새끈한 UI를 제공하기 위해, Windows 1.0 이래로 기본 제공되던 시스템 컨트롤에 추가적으로 도입된 컨트롤들이다.
사용 전에 InitCommonControls 함수 호출이 필요하다지만, 요즘은 공용 컨트롤 (6.0) 매니페스트를 지정하는 것만으로도 초기화가 자동으로 되기 때문에 EXE에서는 이 절차가 굳이 필요하지 않다.

공용 컨트롤들은 완전히 새로운 기능이라기보다는 Windows의 특정 응용 프로그램이나 Office에서 내부적으로 자체 구현으로 돌리던 싸제 컨트롤이 보급품으로 바뀌는 경우가 많다.
이들은 16비트 시절을 경험한 적이 없고 Windows 95/NT 3.51과 역사를 같이하기 때문에, '32비트'를 강조하기 위해 클래스들의 이름이 대부분 32로 끝난다는 특징이 있다. ListView, TreeView 같은 것들은 이런 1기 공용 컨트롤이다.

그 뒤 Internet Explorer 3 이후로 공용 컨트롤은 IE의 버전업을 따라 비약적으로 발전하기 시작했다. 달력 컨트롤, 날짜 선택 컨트롤, ReBar 같은 건 운영체제 보급 컨트롤이라기보다는 뭔가 델파이 컴포넌트 같은 느낌이 드는데.. 이것들은 IE와 함께 도입된 2기 컨트롤이다.

그렇게 새로운 GUI 컨트롤을 만들어서 자기 혼자만 안 쓰고 꼬박꼬박 다른 프로그래머에게도 공개한 건 의도는 좋지만, 그 대신 1990년대 말엔 4~5.x대의 온갖 버전의 comctl32.dll이 난립하면서 Windows가 DLL hell 비판을 받기도 했다. 응용 프로그램이 자신을 기준으로 하는 comctl32.dll을 시스템 디렉터리에다가 막 덮어쓰면서 운영체제의 안정성을 떨어뜨렸기 때문이다.

공용 컨트롤의 3기는 side-by-side assembly라는 방식으로 DLL hell을 종식시키고 GUI가 근본적으로 싹 바뀐 Windows XP와 함께 도래했다. 그리고 3기와 함께 추가된 새로운 공용 컨트롤은 아시다시피 하이퍼링크 컨트롤이다. 인터넷 시대가 도래하면서 하이퍼링크 역할을 하는 컨트롤의 필요성은 예전부터 대두되어 왔으니 말이다.

텍스트 전체가 단일 링크인 게 아니라 A 태그로 둘러싸인 부분만 링크이며, 한 컨트롤 내부에 여러 링크가 있을 수 있기 때문에 더욱 편리하다. A 태그가 없는 하이퍼링크 컨트롤은 그냥 텍스트 static 컨트롤과 별 차이가 없다. 얘는 재래식 InitCommonControls(Ex)가 아니라 오로지 공용 컨트롤 6.0 매니페스트로만 사용 가능하다.

공용 컨트롤들 중에 에디트 컨트롤과 동작이 비슷해 보이는 건 IPv4 주소를 입력받는 컨트롤이 있는데, 내부적으로 자그마한 에디트 컨트롤을 4개 나란히 생성하여 동작한다. 운영체제의 제어판 밖에서는 별로 볼 일이 없는 물건임. IPv6 주소를 입력받을 때는 그냥 일반 에디트 컨트롤을 썼더라.

그리고 잘 쓰이지는 않지만 단축글쇠 입력 컨트롤도 있다. 캐럿도 생성하고 언뜻 보기에 에디트 컨트롤의 서브클래싱 버전 같지만 얘는 에디트 컨트롤을 사용하지 않고 독자적으로 동작하는 물건이다. 사용 가능하거나 반드시 써야 하는 modifier를 Ctrl, Alt, Shift 중에서 지정할 수 있다.
<날개셋> 한글 입력기는 이것들의 좌우 구분이 가능해야 하고 Win키까지도 modifier로 지정 가능해야 하는 관계로, 용도에 맞지 않아서 단축글쇠 규칙 편집 UI에서도 이 컨트롤을 사용하지 않았다.

위의 컨트롤과는 달리 리치 에디트 컨트롤은 공용 컨트롤이 아니다. 얘는 혼자 독자적인 DLL을 갖고서 따로 노는 물건이기 때문에 초기화도 공용 컨트롤과는 다른 방법으로 한다. 복잡한 워드 프로세서를 통째로 컴포넌트화한 것이기 때문에 이것 하나만으로도 다른 어지간한 컨트롤들의 덩치를 모조리 능가한다고 봐야 할 것이다.
예전에도 한번 글로 썼듯이 리치 에디트 컨트롤은 파일 이름과 버전 사이의 관계가 굉장히 이상하게 꼬였다. SxS 방식을 쓰는 것도 아니고.

IE 웹브라우저 컨트롤은 공용 컨트롤이 아닐 뿐만 아니라 일반 윈도우 자체도 아니다. ActiveX 컨트롤이기 때문에 COM API를 써서 훨씬 더 복잡한 방식으로 초기화해서 사용해야 한다. MFC의 도움 없이는 난 불러다 써 보지도 못했다.

comctl32.dll에는 공용 컨트롤을 구동하는 코드가 주로 들어있을 테니 이들을 초기화하는 함수 말고 딱히 다른 기능이 있을까 싶은 생각이 든다. 하지만 기성 대화상자를 변형하여 동작하는 property sheet나 wizard GUI를 구동하는 함수도 여기 있고, 또 image list를 관리하는 함수들도 죄다 여기에 들어있다. 이게 user나 gdi에 들어있지 않고 comctl에 들어있는 이유는, 이 이미지 리스트들은 여러 공용 컨트롤들이 이미지를 표시할 때 한데 공유하는 자료구조이기 때문이다.
그런데 윈도우 컨트롤이 전혀 아닌 물건이 다른 공용 컨트롤과 같은 등급의 카테고리에 문서화돼 있으니 이건 좀 의아한 점이다.

공용 컨트롤들에 대해 본인이 오랫동안 의아하게 생각해 온 점은.. 클래스 이름들의 작명에 일관성이 없다는 점이다. 작명 방식은 크게 세 가지가 있는데, 이것들이 별다른 원칙 없이 뒤죽박죽으로 섞여 있다. 32라는 숫자로 끝난다는 점 말고는 다른 공통점이 없는...데, 그러고 보니 하이퍼링크와 pager 컨트롤은 예외적으로 32가 안 붙었다!

  1. Sys+대문자 계열: SysIPAddress32, Header32, Link, ListView32, TreeView32, TabControl32, Animate32, MonthCal32, DateTimePick32, Pager
  2. msctls_+소문자 계열: msctls_hotkey32, statusbar32, trackbar32, updown32, progress32
  3. 아무 접두사가 없음: ToolbarWindow32, ReBarWindow32, ComboBoxEx32

어지간한 응용 프로그램에서 안 쓰이는 경우가 없는 도구 모음줄과 상태 표시줄만 해도 클래스 이름의 작명 스타일이 (2)와 (3)으로 서로 다르다.

심지어는 소스 코드상으로 클래스 이름을 나타내는 매크로 상수조차도 작명 방식에 통일성이 없다. WC_* 로 시작하는 명칭이 있는가 하면 그냥 *CLASSNAME로 끝나는 명칭도 있다. (toolbar, rebar, statusbar)
서로 다른 팀에서 별개로 만들던 컨트롤들을 한데 합쳐서 이런 일이 생긴 것 같다. 물론 대세는 WC_* 스타일이다.

마소에서도 이런 식의 이름 혼란에 대해서 의식을 전혀 안 하고 있는 건 아니다.
공용 컨트롤들이 사용하는 구조체를 보면 TV_*로 시작하는 구조체가 NMTV*로 바뀌고 예전 명칭은 typedef로 처리되는 등, rename을 종종 하기도 한다. 하지만 처음부터 개명을 할 일이 없게 명칭을 잘 정하는 게 더 좋았을 것이다.

이상이다.
그나저나 공용 컨트롤의 스펙을 다시 보니 옛날에는 Native font control이라는 게 있었던 모양이다. 클래스 이름도 NativeFontCtl이라고 당당하게 있는 윈도우인데.. 도대체 뭘 하는 물건이었지?

The native font control is an invisible control that works in the background to allow a dialog box's predefined controls to display the current system language.


MSDN에 문서화는 이렇게 돼 있지만, 도대체 이런 윈도우를 만들어서 해결하려고 한 문제가 무엇인지.. 그리고 지금은 그게 왜 불필요해졌는지에 대한 의문은 해결되지 않는다. 공용 컨트롤의 세계도 다시 살펴보니 재미있다.

Posted by 사무엘

2015/02/28 08:25 2015/02/28 08:25
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1067

EXE와 DLL의 경계

1.
프로그래밍을 하다 보면 단독 실행이 가능한 EXE 형태의 프로그램만 만드는 게 아니라, 다른 프로그램에 부속물로 붙거나 여러 프로그램들 사이에서 공유되는 라이브러리, 플러그 인 같은 걸 만들 때가 있다.
플러그 인 정도야 호스트 프로그램이라도 분명하게 존재하니 양반이지만, 임의의 프로토콜을 갖는 공용 라이브러리는 static LIB이든 DLL이든, 그 자체로 단독 실행이 가능하지 않다. 그렇다 보니 그 라이브러리를 사용하는 프로그램을 또 별도로 만들어야 해서 테스트와 디버깅이 여러 모로 불편하다.

그래서 Windows에서 DLL을 만드는 솔루션의 경우, 그 솔루션에다가 DLL을 테스트하는 간단한 EXE도 프로젝트로 따로 만드는 게 보통이다.
Visual C++은 지난 2005부터인가 프로젝트를 새로 생성할 때, 솔루션 디렉터리 아래에 동명의 프로젝트 디렉터리가 한 단계 더 생기고, 한 솔루션에 소속된 프로젝트들의 생성물은 다 동일한 output 디렉터리에 만들어지도록 기본 동작 방식이 바뀌었다. obj 같은 임시 파일들만이 프로젝트별로 자기 고유한 위치에 생성된다. 이것은 나름 바람직한 조치라 여겨진다.

2003 이하 2005 이상
프로젝트1\Release\프로젝트1.exe
프로젝트1\Release\프로젝트1.obj
프로젝트1\프로젝트2\Release\프로젝트2.dll
프로젝트1\프로젝트2\Release\프로젝트2.obj
솔루션\Release\프로젝트1.exe
솔루션\Release\프로젝트2.dll
솔루션\프로젝트1\Release\프로젝트1.obj
솔루션\프로젝트2\Release\프로젝트2.obj

그런데, 발상을 전환하면 DLL을 생성하는 소스를 기반으로 곧바로 EXE를 만들어 DLL의 함수들을 의외로 굉장히 간편하게 테스트를 할 수 있다.
링커의 SUBSYSTEM 옵션 하나만 바꿈으로써 WinMain을 사용하는 GUI 프로그램과 main을 사용하는 콘솔 프로그램을 곧바로 전환할 수 있듯, EXE와 DLL은 똑같이 PE 헤더가 있는 실행 파일이며 본질적인 차이가 거의 없다. 구조체 필드 값이 일부 차이가 나고 entry point에서 같이 전달되는 인자의 타입이 다를 뿐이다.

DLL 프로젝트에서 configuration을 하나 만든다. 테스트와 디버그가 목적이므로 Debug 빌드 것을 초기값으로 가져오면 되겠다. configuration 이름은 Debug EXE 정도로 하자.
그 뒤 프로젝트 속성의 General (일반)으로 가서 Target Extension (대상 확장명)은 .dll이던 것을 당연히 .exe로 바꾼다.
그리고 제일 중요한 Configuration Type (구성 형식)을 Dynamic Library (.dll)이던 것을 Application (.exe)으로 바꾼다.

'확인'을 누른 뒤, DLL 소스의 한구석엔 원래의 DLL엔 없던 WinMain 내지 main 함수를 추가하고, 그 안에다 호출하고 싶은 DLL 클래스/함수들을 마음껏 사용하며 테스트한다.
이것만 해 주면 끝이다. 프로젝트를 이 configuration대로 빌드해서 돌리면 된다.

별도의 EXE를 따로 만들어서 테스트를 하는 거라면 그 EXE에 또 테스트 대상 DLL을 로딩하는 코드가 추가되어야 하지만 DLL 자체의 소스로부터 EXE를 생성하면 그런 번거로운 절차가 필요하지 않으니 더욱 좋다. EXE 자체에 DLL의 코드가 그대로 포함되기 때문이다.

static LIB을 만드는 프로젝트도 이런 식으로 별도의 EXE 생성 configuration을 만들어서 테스트가 가능할 것이다.
다만 DLL/EXE와는 달리 static LIB는 링크 절차가 존재하지 않고 그냥 컴파일만 가능하면 라이브러리 파일이 만들어지기 때문에 이로부터 온전한 EXE를 만들려면 추가적인 링커 설정 같은 게 필요할 것으로 보인다.

2.
여담이다만 DLL뿐만 아니라 EXE도 DLL처럼 export 심벌을 가질 수 있으며 그걸 GetProcAddress를 통해 얻어 올 수 있다.
EXE만 자신이 로딩한 플러그 인 DLL로부터 함수를 얻어 오는 게 아니라, DLL 역시 자신을 로드한 EXE로부터
GetProcAddress( GetModuleHandle(NULL), "GetHostInfo") 이런 식으로 코드를 얻을 수 있다. 이것도 참 기발한 발상이 아닐 수 없다. 어디 활용할 데가 없을까?

내가 개인적으로 굉장히 놀란 것은, 저렇게 한 프로세스 공간의 주인 역할을 하는 EXE가 아니라..
완전히 다른 EXE를 로딩해서 거기에 있는 코드를 실행하는 것도 가능하다는 것이다. EXE는 보통 0x400000 같은 고정된 주소에 로드되며 재배치 정보가 존재하지 않기 때문에 자기 위치에 로드가 못 되면 로딩이 실패한다.

그런데 자신과 로드 주소가 겹치는 EXE도 LoadLibrary를 하면 일단 작업이 성공하며 리소스 추출뿐만이 아니라 GetProcAddress도 실행 가능한 듯하다. 이쯤 되면 EXE와 DLL의 경계가 어찌 되는지가 궁금해진다.

3.
아무 중간 계층 없이 C/C++ 언어만으로 뭔가 라이브러리를 남에게 제공하는 건 애로사항이 적지 않다.

  • 디버그 or 릴리스?
  • 32 or 64비트?
  • 최종 형태는 DLL or LIB?
  • VC++ 어느 버전? (보안 기능 링크 에러)
  • 사용하는 CRT의 형태는 DLL or static?

이런 식으로 상호 일치해야 하는 변수가 급격히 늘어나기 때문이다. 조건부 컴파일이 괜히 필요했던 게 아니다.
C/C++ 런타임 라이브러리도 비주얼 C++의 버전이 바뀜에 따라 내부적으로 야금야금 더해지고 바뀌는 기능이 있기 때문에--특히 보안 관련-- static 링크하는 경우 빌드 툴의 버전이 안 맞으면 이상한 심벌명에서 링크 에러가 나고 각종 문제가 생기기 쉽다.

그나마 같은 비주얼 C++끼리이니까 망정이지 서로 다른 컴파일러끼리 C++ 클래스 라이브러리를 공유한다면 name decoration까지 문제가 됐을 것이다. 사실상 공유 불가능이다.
옛날에는 문자 집합의 크기(일명 유니코드/ANSI)조차도 변수가 따로 있었을 정도이지만 요즘은 그래도 유니코드, 정확히는 wide string만 고려하면 되니 그건 그나마 나아졌다.

이 문제가 워낙 복잡하니..
일차적으로는 COM 같은 바이너리 표준이 나왔을 것이다.
아니면 그냥 소스 코드를 통째로 넘겨줘서 필요한 사람이 알아서 빌드해서 쓰게 하든가. 그 라이브러리가 애초부터 오픈소스 진영의 작품이라면 다행이지만, 상업용 코드라면 인터페이스 부분을 제외한 나머지에다가는 난독화 처리가 필요할 것이다.

그것도 싫으면 저런 골치아픈 요소들을 싹 잊어버리고 자바/C# 같은 바이트코드 기반으로 가는 수밖에 없는데... 그건 물론 성능은 COM보다도 엄청나게 더 희생시킨 귀결일 것이다.
그래도 아무 클래스에나 public static void Main만 있으면 그게 곧 실행 가능한 물건이고 빌드 속도도 안드로메다 급으로 빠르며 골치 아픈 32/64비트 구분 같은 것도 없는 환경이.. C++ 프로그래머로서 참 부럽게 느껴질 때가 있다.

Posted by 사무엘

2014/12/31 08:30 2014/12/31 08:30
, , , ,
Response
No Trackback , 10 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1045

1. disabled 스타일 개론

소프트웨어 GUI에서 대화상자나 메뉴 같은 구성요소를 보면, 상태를 나타내는 속성 중에 논리값으로 enabled 여부라는 게 존재한다.
이게 false여서 disable된 물건은 비록 화면에 보이긴 하지만 흐리게 표시되며 완전히 없는 물건으로 취급된다. 사용자의 키보드나 마우스 조작에 반응을 하지 않으며 선택할 수도 없다.

Windows 운영체제에서는 윈도우에서 WS_DISABLED라는 스타일 비트가 이런 역할을 한다. 기본 스타일에 이 비트가 지정되어 있는 윈도우는 키보드 포커스를 받을 수 없으며, 거기에다 대고 마우스 포인터를 움직여도 통상적인 WM_MOUSEMOVE, WM_?BUTTONDOWN 같은 메시지가 오지 않는다.

즉, availability는 어느 정도 운영체제가 직접 관리를 해 주는 예약된 속성이다.
어떤 윈도우가 enabled인지 여부를 알려면 IsWindowEnabled 함수를 호출하면 된다.
IsWindowEnabled(hWnd)는 !(GetWindowLongPtr(hWnd, GWL_STYLE)&WS_DISABLED)와 동치라고 생각하면 된다.

enabled 여부를 설정하는 함수는 EnableWindow이다. 이때 대상 윈도우는 WM_ENABLE라는 메시지를 받음으로써 자신의 availability 속성이 바뀌었다는 통지를 받는다.
Get과 마찬가지로 SetWindowLongPtr를 통해 스타일을 수동으로 바꿔 줘도 거의 같은 효과를 낼 수 있다.
단, 이 방법은 간단히 전용 함수를 호출하는 것보다 번거로우며, 이렇게 속성이 바뀌면 대상 윈도우는 WM_STYLECHANGED만을 받지 WS_DISABLED 비트에 차이가 있더라도 WM_ENABLE 메시지가 오지는 않는다.

2. 화면에 그리기

대화상자 컨트롤이라면 WM_ENABLE 메시지가 왔을 때 자신을 화면에 다시 그리는 처리를 한다.
가령, 평소에는 COLOR_WINDOWTEXT라는 시스템 색상으로 글자를 찍은 반면, disable된 뒤부터는 COLOR_GRAYTEXT 색상으로 글자를 다시 찍는다.

지금이야 Windows 8 때부터 고전 테마라는 게 사라져서 점차 과거의 유물이 돼 가지만..
옛날에 Windows UI를 보면, 메뉴나 도구모음줄에서 사용할 수 없는 항목은 글자가 단순히 회색이 아니라 흰색 위에 회색이 깔려서 뭔가 음각 엠보싱처럼 그려지곤 했다.

사용자 삽입 이미지

그거 처리를 해서 disable 상태를 화면에 표현하는 건 DrawState라는 함수 호출 한 방이면 바로 된다. 이건 딱 회색 3D 대화상자 스타일이 도입된 Windows 95와 NT4에서 첫 추가된 함수이다. 게다가 텍스트와 비트맵(아이콘) 모두 그렇게 그리는 걸 지원한다.

비트맵의 경우, 마스크 비트맵을 바탕으로 엠보싱을 만들며, 이 테크닉은 지금도 그대로 쓰인다. 그렇기 때문에 요즘은 32비트 비트맵 내부의 알파 채널이 투명색을 대신하는 지경이 되었음에도 불구하고 DrawState 함수로 disable 상태의 엠보싱 아이콘을 그리려면 비트맵에 모노크롬 흑백 배경 마스크 비트맵도 넣어 줘야 한다.
뭐, 궁극적으로는 트루컬러 아이콘이라면 구시대스러운 비트 연산이 아니라, 투명도를 높이고 채도를 낮춰서 그림을 더 엷고 탁하게 만드는 '현대적인' 방식으로 disable 상태를 그려야 하겠지만 말이다.

3. disabled 윈도우의 또 다른 용도

그러면 이런 disabled 속성은 오로지 대화상자 내부의 컨트롤 같은 WS_CHILD급 윈도우에서만 쓰이는가 하면 그렇지 않다. 타 윈도우의 자식이 아니라 top-level이 될 수 있는 WS_POPUP급 윈도우도 WS_DISABLED 속성을 줘서 생성할 수 있다. 이 윈도우는 화면이 달랑 떠 있기만 하지 사용자가 포커스를 줘서 키보드 입력을 할 수가 없게 된다.

사실, 한 프로세스 안에서 child 윈도우는 클릭했다고 해서 딱히 포커스가 자동으로 거기로 옮겨지지는 않는다. 포커스가 가는 건 해당 윈도우가 WM_LBUTTONDOWN이 왔을 때 SetFocus를 자체적으로 호출했기 때문이다.
그러나 소속된 프로세스가 다른 top-level 윈도우의 경우, 클릭하면 일단 그 창으로 WM_ACTIVATE 메시지가 가고 컨텍스트 전환이 발생한다. 이것은 프로그램의 의사와 무관하게 운영체제가 일방적으로 하는 일이었는데, disabled 윈도우는 그걸 막을 수 있다.

물론 disabled 윈도우는 앞서 말했듯이 정상적인 마우스 메시지가 오지 않는데, 그래도 이것도 받는 방법이 있다.
disabled라고 해도 top-level 윈도우에는 기본적으로 마우스 포인터 설정을 위해서 WM_SETCURSOR 메시지 정도는 온다. 이 메시지의 lParam에는 이 윈도우에 원래 오려고 한 메시지가 담겨 있기 때문에 이를 토대로 비록 disable 상태이지만 마우스 동작에 반응을 하도록 프로그래밍이 얼마든지 가능하다. child 윈도우가 아닌 top-level 윈도우이기 때문에 이런 메시지가 온다.

disabled 편법을 쓰지 않고 '클릭해도 반응만 하지 포커스가 바뀌지는 않는 간단한 윈도우'라는 개념은 비교적 늦게 등장했다. Windows 2000부터 WS_EX_NOACTIVATE라는 확장 스타일이 정식으로 도입된 것이다. 이런 윈도우는 ShowWindow에다가도 단순히 SW_SHOW가 아니라 SW_SHOWNOACTIVATE를 줘야 한다.

4. 상태 변경과 관련된 연계 작업들

대화상자에서 컨트롤을 enable/disable시키는 상황은 크게 선천적인 것과 후천적인 것으로 나뉜다. 전자는 WM_INITDIALOG에서 단 한 번 enable 여부가 결정되고 그 뒤에 대화상자가 닫힐 때까지 그 상태가 바뀔 일이 없는 경우를 말한다.
후자는 한 컨트롤의 조작 여부나 값에 따라 다른 컨트롤의 enable 상태가 인터랙티브하게 수시로 바뀌는 경우를 가리킨다. 예를 들어 라디오 버튼이 특정 항목으로 맞춰져 있을 때만 조건부로 사용 가능해지는 하부 조건 체크박스 같은 것. UI 프로그래밍을 해 본 분이라면 이 분류가 수긍이 갈 것이다.

그런데 이렇게 컨트롤들의 상태를 바꾸는 건, 단순히 한 윈도우에 대해 EnableWindow를 호출하는 것 이상으로 이와 결합된 반복 패턴이 여럿 존재한다.

(1) 첫째, 대상 컨트롤의 이전에 있는 label 컨트롤을 같이 enable/disable시키는 경우이다. 대상 컨트롤이 버튼이라면--push, check, radio 모두 포함-- 그 자체가 &로 시작하는 Alt 액셀러레이터 글쇠를 갖는다. 그러나 나머지 edit, list, combo 박스 같은 것들은 자신의 액셀러레이터가 없으며 그 이전의 static 라벨로부터 액셀러레이터를 넘겨받는 형태이다.

따라서 그런 컨트롤이 disable됐다면 자기의 앞의 컨트롤도 같이 disable되는 게 이치에 맞다. 앞의 단순 label은 보통 독자적인 컨트롤 ID가 없이 그냥 IDC_STATIC인 경우도 많으므로 핸들값을 GetWindow(hCtrl, GW_HWNDPREV)로 얻어 오는 수밖에 없다.

(2) 둘째, 대상 컨트롤을 화면에서 감출 때에도 ShowWindow(hCtrl, SW_HIDE)만 할 게 아니라 disable을 시켜 줘야 한다. 왜냐 하면 enable 상태인 컨트롤은 비록 화면에 없더라도 Alt 액셀러레이터에는 반응을 해서 사용자가 여전히 기능 접근이 가능하기 때문이다.
본인은 개인적으로는 이게 바람직한 설계가 아니라고 생각하며, Windows가 왜 그렇게 동작하는지 알지 못한다. 하지만 어쨌든 Windows 95부터 8.1에 이르기까지, 조건부로 컨트롤들을 보였다가 숨겼다가 하는 UI의 경우, 감춰지는 컨트롤은 disable도 시켜 줘야 한다.

(3) 셋째, 지금 키보드 포커스를 받고 있는 컨트롤이 disable되는 경우, 포커스를 자신의 다음 컨트롤로 옮기는 일을 해당 프로그램이 수동으로 해 줘야 한다. 이걸 안 해 주면 그 컨트롤이 disable된 뒤부터 키보드 상태가 꼬여 버린다.
이 단서의 주된 적용 대상은 push-버튼이다. 대표적인 예로는 프로퍼티 페이지에 있는 '적용' 버튼. 이 버튼을 누른 순간 이 버튼은 사용 불가 상태가 되며, 사용자가 다른 설정을 또 건드려 줘야 다시 사용 가능해지니 말이다.

본인의 개인적인 생각은 이것도 역시 운영체제가 자동으로 처리해 줘야 하는 게 아닌가 싶지만, 현실은 시궁창이다. 생각보다 자비심이 없다..;;
대화상자에서 특정 컨트롤에다 포커스를 주는 건 SetFocus를 해서는 안 되며, 대화상자 부모 윈도우에다가 WM_NEXTDLGCTL 메시지를 보내는 방식으로 해야 한다. 그렇게 해야 대화상자의 default 버튼에 굵은 테두리가 그려지는 처리가 올바르게 된다.

그래서 본인은 위의 세 시나리오를 모두 감안하여 대화상자 컨트롤을 enable/disable시키는 함수를 별도로 만들어서 사용하고 있다.
그림을 좀 더 곁들이면 전반적으로 설명하기가 더 편하겠다는 생각이 드는데.. 귀찮아서 생략한다. ㄱ-

Posted by 사무엘

2014/12/08 08:29 2014/12/08 08:29
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1037

회사 업무 때문에 구질구질한 재래식 Windows API 기반 네이티브 데스크톱 프로그램이 아니라 일련의 신문물들을 접할 일이 있었다. 바로 지금까지 말로만 듣던 Windows Phone 플랫폼 개발 때문이었다.

1. Windows 8.1

Windows 8로 넘어가면서 부팅부터 UEFI라는 기술이 도입되면서 뭐가 좀 바뀌었다. 운영체제를 다시 설치하려고 부팅 디스크 탐색 순서를 바꾸려고 해도 BIOS Setup에서 좀 번거로운 절차를 거쳐야 하게 되었다.

Windows Phone 에뮬레이터를 돌리려면 역시 BIOS Setup을 들어가서 기본적으로 꺼져 있는 CPU 가상화 기능을 켜야 하며, OS도 아무거나 쓰면 되는 게 아니라 8.1 Pro 이상급이 반드시 필요하다. Hyper-V 기능이 home급에서는 지원 안 되고 Pro나 엔터프라이즈 급 이상부터만 지원되기 때문이다.
Pro 이상에서만 지원되는 대표적인 기능이 바로 원격 데스크톱 서버 기능인데.. 그런 비슷한 기능이 하나 더 생긴 것이다.

요즘은 스마트폰 CPU도 PC와 별 차이 없을 정도로 굉장한 고사양이기 때문에 에뮬레이션을 위해서는 CPU 차원에서의 온갖 첨단 기능이 덩달아 필요해진 듯하다. 덕분에 이젠 가상 머신에서도 Windows Aero 효과가 돌아가는 세상이 되기도 했다.

Windows 8 이상의 그 각지고 단순해진 GUI를 보면, 비스타/7에 비해 퇴화한 것 같고 화면을 왜 저렇게 만들었나 싶은 생각이 처음에 들었다. Windows 8부터는 아시다시피 고전 테마가 없어지고 화면 scheme은 오로지 "표준 아니면 고대비"로 극도로 단순화됐다.
하지만 이젠 저것도 그럭저럭 적응이 돼 간다. 외형이 단순해진 대신, 단조로움을 덜기 위해 창틀의 색깔이 시시각각 변하는 기능이 생기기도 했고. ㅎㅎ

운영체제를 설치하는 중에 전체 화면에서 배경색이 서서히 알록달록하게 변하는 건, 마치 도스 시절 VGA mode 13h에서 전체 화면 게임 프로그램이 팔레트 스크롤을 하는 것 같은 느낌이 들었다.

2. Visual C++ 2013

외형이 별로 바뀐 게 없고 시간 간격도 2012에 비해 겨우 1년 차이밖에 안 나는지라 변화량을 만만하게 봤었는데, 실제로 써 보니 그렇게 만만하지는 않다. 아기자기한 기능들이 많이 강화되고 좋아졌다.

색깔 scheme이 하양과 검정 말고 '파랑'이 하나 더 생겼는데 파랑은 옛날의 우중충한 2010 분위기를 내는 스타일이어서 개인적으로는 비호감.
운영체제의 기본 컨트롤을 쓰는 게 아니라 모든 걸 자기가 직접 그린다는 특성상, 스크롤 바가 굉장히 똑똑해졌다. cursor가 속한 줄 위치가 스크롤 바에도 언제나 표시되어 나오고, 스크롤 중에 페이지 썸네일을 표시하는 기능도 있다.

옵션(프로젝트 옵션과 프로그램 옵션 모두) 대화상자가 드디어 크기 조절이 가능해졌으며, C++도 코딩 중의 자동 서식과 자동 완성 기능이 제법 강화되었다.

Visual Studio (C++ 포함)는 지난 2005 버전 때부터 Express라는 무료 버전이 정식으로 배포되고 있다. 그래서 예전에는 플랫폼 SDK(= 무료 배포)도 자체적으로 컴파일러를 포함하고 있었는데 그것까지 express 에디션으로 완전히 대체되었다. 상업용 버전과는 달리 2013 Express 버전은 Windows 8용 Metro/Phone 앱만 만들 수 있는 'for Windows' 에디션과, 예전의 재래식 native 프로그램만 만들 수 있는 'for Windows desktop' 에디션이 따로 나뉘었다.

3. C++/CX

드디어 그 이름도 유명한 '요물'을 만져 보게 됐다. 처음에는 단순히 C++을 닷넷용으로 마개조한 Managed C++와 C++/CLI의 후신인줄로만 알았는데 그렇지 않다. C++/CX와 Windows RT API는.NET 내지 C++/CLI하고 무늬는 비슷하지만 내부 구조는 완전히 다르다.
예를 들어, 옛날의 C++/CLI에서는 일반 C++ 개체(new)와 관리되는 새로운(__gc new) 개체는 서로 has-a 관계조차도 맺을 수 없었다. 취급되는 방식이 서로 다른 개체를 다른 개체의 멤버로 가질 수 없었다는 뜻이다. C++/CX는 그런 제약이 없다.

.NET 그쪽 바닥은 전통적인 바이트코드 기반 런타임이지만 C++/CX는 엄연한 네이티브 코드 기반이다. 가장 큰 차이로 후자에는 garbage collector가 없다. ref new로 할당하는 ^ 라는 이상한 포인터가 있긴 하지만 얘는 내부적으로 그냥 레퍼런스 카운팅으로 관리될 뿐이다. .NET과 비슷한 API를 차용하고, C#에서 partial도 가져오고 델리게이트나 boxing 같은 것도 가져왔지만, 내부는 여전히 native라는 게 참 인상적이다. 게다가 이제 퇴물 신세가 됐나 싶던 COM 인터페이스까지 다시 끄집어냈다니!

Visual C++ 200x 시절에만 해도 이제 MS가 C++을 버렸네(특히 MFC!!), 네이티브 코드 시절이 끝났네, 심지어 Windows 차기 버전은 닷넷 같은 바이트코드 기반으로 완전히 새로 만들어진다네 하는 온갖 낭설이 떠돌았는데.. 201x로 와서는 그런 낭설이 완전히 불식된 듯한 느낌이다. MFC는 2008 feature pack 때부터 잘 알다시피 환골탈태하였으며, C++ 언어 자체도 C++11 같은 온갖 확장 규격에 힘입어 한없이 강력하고 복잡해졌다. 거기에다 Windows RT의 코드 기반도 네이티브 코드에 힘을 실어 줬으니 C++은 예나 지금이나 건재한 언어 인증을 하게 됐다.

이런 요물의 등장으로 인해 도리어 .NET의 위상이 굉장히 어중간해졌다. 비슷한 시기에 등장한 GDI+도 너무 금세 버림받고 낙동강 오리알 신세가 됐고 말이다. 얘는 이제 하드웨어 가속 지원도 못 받는다니, 안티앨리어싱이 되는 그래픽이 필요하다면 얄짤없이 Direct2D라도 새로 공부해야 하게 생겼다.

뭐 내부 메커니즘이야 어떻든, 네이티브 코드 C++에서도 delete 따질 필요 없이 new를 막 남발해도 된다는 게 무척 신기하며, 마치 자동차로 치면 수동을 몰다가 자동을 모는 듯한 느낌이다. 하지만 세상에 다 썼으면 반드시 반납을 해야 하는 리소스가 메모리만 있는 건 아닌데, 파일이나 다른 커널 오브젝트들은 어떻게 관리되며 생명 주기가 어떻게 되는지 궁금해질 때도 있다. 참고로, 레퍼런스 카운팅도 GC에 비해서 마냥 가볍고 편리하기만 한 물건은 아니며 약점이 있다.

Windows RT API들은 정말 복잡한 namespace와 클래스, 추상 계층들이 넘쳐난다. 지저분한 Windows API를 정말 허접하게 감싼 MFC 정도를 생각했다가 요즘 프레임워크들을 보면 입이 떡 벌어질 수밖에 없다.
멤버뿐만 아니라 클래스 자체에다가도 public 같은 접근성을 지정할 수 있으며, 더 상속이 안 되게 하는 sealed 속성을 줄 수 있다. 일반 C++에서는 허용되지만 C++/CX에서는 “무슨 클래스에서는 생성자가 public이어서는 안 된다, 데이터 멤버가 뭐여서는 안 된다”는 식의 까다로운 제약도 굉장히 많아서 처음엔 답답함을 느꼈다. (상속이라는 게 없는 언어에다가도 클래스를 제공할 수 있게 하기 위해 들어간 제약이라 함.)

이 기회에 delegate라는 게 뭔지도 다시 살펴보게 되었다. 선언 자체는 C++로 치면 함수 포인터 내지 멤버 함수 포인터에 대한 typedef를 선언하는 것과 비슷한 개념이며, 이놈의 인스턴스는 따로 new로 선언해야 한다. 그때 생성자에다가 다른 함수 명칭라든가 람다 함수를 집어넣어 주면 된다.
람다의 경우 this를 캡처로 주면 자연스럽게 멤버 함수도 대리자가 될 수 있으니 매우 유연하다. 물론 그 유연성은 성능 대가를 치르고 얻어진 것이겠지만 말이다.

Windows RT API에는 비동기적으로 행해지는 동작이 많으며, Concurrency Runtime 라이브러리와 밀접한 관계가 있다. 표준 C++ 라이브러리의 일부인 모양인데 create_task에다가 할 일들을 넣어 주고, 그 일이 반드시 다 끝난 뒤에 수행할 일은 저 함수의 리턴값에다가 .then 메소드를 호출하고 거기에다가 또 람다 형태로 넣으면 된다. 기본적으로 코딩 패턴이 그러하다.
.wait 메소드를 이용해서 동기화를 시켜도 되지만, 이 경우 Windows Phone은 UI 스레드까지 멈춰 버려서 데드락이 발생하는 듯하다. 참고로 C#의 경우 언어 차원에서 await이라는 전용 키워드가 존재한다고 함.

도대체 저 라이브러리는 어떻게 구현되었나? 소스 내부에 CreateThread, WaitForSingleObject 같은 Windows API라도 썼는지 궁금했지만.. 디버거로 내부 추적이 전혀 되지 않을 뿐더러 온갖 암호 같은 복잡한 템플릿은 도저히 분석 가능하지 않았다. 그래서 분석을 포기했다.
아무튼, C++은 람다 함수가 도입되어서 코드를 값으로 집어넣는 게 가능해지고, 이게 템플릿과도 결합하는 바람에 그야말로 예전의 C++에서는 상상도 할 수 없던 무궁무진한 활용이 가능해지긴 했다.

Windows RT 환경에서는 재래식 Windows API는 쓸 수 있는 것도 있고 그럴 수 없는 것도 있다. 이걸 일일이 다 분류하는 것도 마소의 엔지니어들의 입장에서는 엄청 고된 일이었겠다.
실행을 잠깐 멈추는 Sleep 함수도 누락되고 없기 때문에 Concurrency::wait를 써야 한다고 한다. 난 저걸 알기 전에는 이벤트 오브젝트를 만들어서 WaitForSingleObjectEx 함수를 쓰곤 했다.

끝으로, Windows RT의 C++/CX 환경은 네이티브 코드를 표방하는 관계로 재래식 static library와 DLL을 모두 만들어 쓸 수 있다. 단, 불가능하지는 않지만 static library의 경우 링커가 경고를 띄운다. 그건 오로지 같은 C++ 프로젝트에서만 활용 가능하니 재사용성이 크게 떨어지기 때문이라고.

RT 환경에서는 Windows Runtime Component라는 특수한 형태의 DLL을 만들어서 코드를 재사용하는 것이 권장되는 방법이다. Windows RT계의 COM 같은 물건인데, 그렇다고 COM 정도로 문법이 크게 제약되고 경직된 건 아니다. C#으로 표현 가능한 언어 요소들을 모두 표현할 수 있고, 아무래도 원시적인 인클루드와 라이브러리보다는 더 깔끔한 빌드/재사용 시스템인 듯하다.

이런 것들을 경험하고 나니 뭔가 미래에 갔다 온 듯한 느낌이었다.
XAML은 Win32 개발로 치면 rc 파일 같은 것이고
public ref class는 Win32에서 __declspec(dllexport) 같은 건가?

예나 지금이나 완전한 형태의 Windows 프로그램을 만들기 위해서는 무슨 언어든 문법 확장이 불가피했지만 지금은 더 체계적이고 조직적이고 더 노골적으로 하는 듯하다.
시대를 불문하고 불변인 자기만의 전문 영역이 있어야겠지만, 한편으로 최신 시대 조류도 놓치지 말고 따라갈 줄 알아야겠다는 생각이 새삼 들었다.

'독립 개발자 네트워크'를 운영하고 계신 깁뿔 님께서 오래 전에 Windows 8 개발 공부를 하면서 올려 놓으신 팁들을 이 기회에 뒤늦게나마 유용하게 활용했다.

Posted by 사무엘

2014/10/07 08:36 2014/10/07 08:36
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1015

« Previous : 1 : ... 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : ... 13 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
2663655
Today:
830
Yesterday:
1553