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

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

예전에 에디트 컨트롤에 대해서 글을 한번 쓴 적이 있었는데 그것들 말고도 또 재미있는 이야깃거리가 많아서 글을 추가로 올리게 됐다.

1.
Windows의 에디트 컨트롤에는 ES_AUTOHSCROLL, ES_AUTOVSCROLL이라는 옵션이 있어서 이 옵션이 없으면 에디트 컨트롤은 가로나 세로로 스크롤이 되지 않는다. 그리고 스크롤만 안 되는 게 아니라 지금 화면 영역을 벗어나는 크기로는 텍스트가 입력 자체가 전혀 되지 않게 된다. 가령, 가변폭 글꼴을 쓴다면 W는 몇 개 입력 못 하지만 i는 꽤 많이 집어넣을 수 있다.

차라리 W든 i든 글자 수 자체에 대한 제약을 거는 거라면 모를까, 저런 제약 기능이 실생활에서 쓸 일이 있는지는 본인은 좀 회의적이다. 한 줄짜리 에디트 컨트롤이 메모리 상의 글자 수도 아니고 픽셀 길이가 초과했는데 스크롤이 안 되는 경우는 거의 찾을 수 없기 때문이다. (물론, 글자 수에 제약을 거는 방법은 EM_SETLIMITTEXT라고 방법이 따로 있긴 하다)

2.
에디트 컨트롤은 잘 알다시피 자체적으로 Ctrl+C, X, V 글쇠를 처리하여 텍스트에 대한 Copy/Cut/Paste 기능을 제공한다. 그런데 운영체제나 프로그램에 따라서는 "텍스트 전체 선택"을 의미하는 Ctrl+A도 지원되는 것 같기도 하고 안 되는 것 같기도 하다. 도대체 어찌 된 일일까?

실상은 이러하다. 내가 여러 조건을 달리하여 실험을 해 보니, 공용 컨트롤 6.0이 제공하는 새로운 에디트 컨트롤만이 single-line 방식에 한해서 Ctrl+A도 자체 처리한다. 나머지 일반 에디트나 multi-line 에디트는 아마 호환성 차원에서 이를 지원하지 않는다.

물론, 응용 프로그램이 Ctrl+A를 액셀러레이터에다 등록해서 자체적으로 에디트 컨트롤에다가 EM_SETSEL(0, -1)을 날려 준다면 어디서나 Ctrl+A가 동작하게 된다. 컨트롤이 아니라 그 컨트롤을 사용하는 응용 프로그램이 직접 Ctrl+A를 구현한 대표적인 예는 바로 메모장이다.

3.
에디트 컨트롤은 자신이 키보드 포커스를 받으면 텍스트 전체를 선택해 놓는다. 사용자가 기존 텍스트를 완전히 무시하고 입력을 새로 시작할지, 아니면 기존 입력을 그대로 유지하거나 살짝만 고칠지를 선택 가능하게 하자는 차원에서이다.
그런데 대화상자에서 굳이 tab 키로 포커스를 바꿨을 때뿐만이 아니라 Alt+? 액셀러레이터를 눌렀을 때도 이 동작이 일어나며, 심지어 지금 포커스를 받고 있는 동일한 컨트롤을 Alt+?로 다시 선택했을 때도 동일한 동작이 일어난다. 이것은 WM_SETFOCUS나 WM_KILLFOCUS하고는 관계가 없는 동작인데 어떻게 이런 일이 가능할까?

정답은 에디트 컨트롤이 WM_GETDLGCODE 메시지에 대해서 DLGC_HASSETSEL 비트 플래그를 되돌리기 때문이다.
대화상자는 자기 밑에 있는 컨트롤들에 대해서 이런 세부적인 메시지를 보내어 정보를 파악하는데, 저 플래그가 있는 컨트롤은 문자열 입력란으로 간주하여 액셀러레이터 키를 받았을 때도 EM_SETSEL 메시지를 보내 준다. 저 플래그만 쓰면, 운영체제의 표준 에디트 컨트롤이 아니어도 똑같은 동작을 하는 컨트롤을 얼마든지 만들 수 있다. <날개셋> 한글 입력기의 자체 에디트 컨트롤도 당연히 이 방식을 따랐다.

Posted by 사무엘

2014/09/20 08:38 2014/09/20 08:38
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1009

1. WM_QUERYDRAGICON 메시지

제목에 언급돼 있는 저 메시지는 도대체 뭘 하는 물건일까?
얘는 20여 년 전에 Winows 95가 등장한 이래로 쓸 일이 사실상 전혀 없어진 잉여이다.

그 주된 이유로는 첫째, 그때부터 minimized icon이라는 개념 자체가 운영체제에서 완전히 없어졌기 때문이다.
95 이래로 바탕 화면에는 '내 컴퓨터'나 '휴지통' 같은 걸 제외하면, 바탕 화면이라는 디렉터리에 있는 파일들만이 표시된다. 자주 쓰는 프로그램의 바로가기 정도나 바탕 화면에 표시되며 그것들도 엄밀히 말해서는 그 디렉터리에 있는 파일의 일종인 것이다.

최소화된 프로그램은 작업 표시줄의 제목 말고는 화면에 아무것도 보이지 않는다. 시작 메뉴와 작업 표시줄을 구동하는 explorer 셸 자체가 죽었다면 최소화된 프로그램이 진짜로 제목 한 줄만 달랑 보이는 최소화 상태로 있을 수 있지만, 이건 운영체제가 완전 막장이 됐을 때에나 발생하는 상황이며, 그냥 그 제목 텍스트를 드래그하면 되지 별도의 드래그용 아이콘이 필요하지는 않다.

둘째로, WM_SETICON / WM_GETICON이 그나마 남아 있던 아이콘 관련 기능을 완벽하게 대체해 버렸기 때문이다.
클래스를 등록하던 시절에 대표 아이콘이 지정되지 않았던 윈도우라 하더라도 가끔은 외형에 별도의 custom 아이콘이 필요할 때가 있다. 대화상자 단독으로 달랑 실행되는 프로그램이 대표적인 예다. 대화상자는 윈도우 프로시저는 거의 언제나 우리가 지정한 custom 버전이 쓰이지만, 그 윈도우 자체의 클래스 등록은 우리가 한 게 아니기 때문이다.

Windows 3.x 시절에는 창의 아이콘이 표시될 때가 최소화됐을 때 정도밖에 없지만, 95부터는 창 제목 왼쪽의 시스템 메뉴가 있는 곳에 창의 아이콘이 언제나 표시되어 있다. 그렇기 때문에 클래스 아이콘과 다른 아이콘을 별도로 공급하는 것은 운영체제가 나중에 응용 프로그램에다가 메시지를 보내는 형태가 아니라, 응용 프로그램이 사전에 운영체제에다 메시지를 보내는 것으로 디자인이 바뀌었다. 그 변경의 산물이 바로 WM_SETICON. 이 아이콘이 대외적으로 표시되며 심지어 Alt+Tab을 누른 동안 프로그램 리스트에도 뜨게 된다.

그럼에도 불구하고 MSDN과 구글 따위를 뒤져 보면, 이 메시지에 대해서는 20년도 더 전에나 유효하던 낡은 설명만이 기계적으로 그대로 소개되어 있으며, 이 정보는 오늘날 outdated됐다는 말은 어디에도 없다. 심지어 Visual C++ 2012의 MFC 마법사에서 대화상자 기반 응용 프로그램을 만들면, CDialog(Ex)의 파생 클래스는 저 메시지에 대한 핸들러도 여전히 참 친절하게도 만들어 준다. 뭐지 이건..?

2. 잉여 WM_SIZE 파라메터

잉여 요소가 의외의 가까운 곳에 또 있다.
WM_SIZE야 Windows 프로그래머치고 모르는 사람이 있을 수가 없는데.. wParam에는 최소화/최대화와 관련된 부가 정보가 따라온다. 최소화되었다면 SIZE_MINIMIZED가, 최소화되었다면 SIZE_MAXIMIZED가 오며, 그 밖의 일반 상황에서는 SIZE_RESTORED (0)가 된다. 딱 이 정도만 알고 있으면 된다.

그런데
SIZE_MAXHIDE: Message is sent to all pop-up windows when some other window is maximized.
SIZE_MAXSHOW: Message is sent to all pop-up windows when some other window has been restored to its former size.

라고 문서화돼 있는 이 값이 온 걸 본 적 있으신 프로그래머는 한번 손 들어 보시길..
저 조건을 최대한 만들어서 디버거 붙이거나 Spy++로 확인해 봐도 저런 건 좀체 안 온다.
어떤 프로그램 창이 최대화됐거나 해제됐다고 해서 다른 프로그램 창에 저 메시지가 올 거라고 생각한다면 경기도 오산이다.

구글, MSDN 다 뒤져도.. 저 기계적인 설명 말고 다른 용례는 안 나온다. 외국의 포럼에서 딱 하나 질문이 올라온 게 있긴 한데, 딱히 답변 없다. (☞ 링크 클릭)

Windows 운영체제의 레거시들 분석에는 세계 톱급의 전문가라 할 수 있는 레이몬드 챈 아저씨의 블로그, MFC와 Windows GUI 프로그래밍에서 한 가닥 했던 Paul DiLascia 등.. 거기에 설명이 없으면 아무데도 없는 거다.;;
나도 진지하게 굉장히 궁금하다. 저 설명과 매크로 상수값은 그저 잉여인지를? WINE 같은 데서는 저게 실제로 구현돼 있을까?

Posted by 사무엘

2014/08/22 08:21 2014/08/22 08:21
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/998

라벨, 에디트, 리스트 박스, 콤보 박스, 일반 버튼, 라디오/체크 박스.
이름만 들어도 친숙한 이 물건들은 응용 프로그램의 대화상자에 들어가는 그야말로 필수 GUI 구성요소들이다.

여기에다 리스트 컨트롤, 트리 컨트롤 같은 더 고급스러운 공용 컨트롤까지 있으니 실무에서는 사실 99%에 가까운 일은 기존 컨트롤만으로 다 해치울 수 있다.
사소하게 마음에 안 들고 동작 방식을 좀 고쳐야 하는 건, (1) 이들이 제공하는 owner-draw 옵션(주로 외형 외주) 내지 일부 메시지에 대한 (2) 윈도우 프로시저의 서브클래싱으로 customize 가능하다.

그러나 아주 가끔은, 기존 컨트롤과는 동작 방식이 완전히 다른 새로운 윈도우를 내가 머리부터 발끝까지 직접 구현하여 대화상자에다 집어넣어야 하는 경우가 있다. 기존 컨트롤을 약간만 고치는 정도로는 성이 안 찬다.

본인이 개발한 <날개셋> 한글 입력기의 GUI를 보면 이런 custom 컨트롤들이 적지 않게 보인다. 당장 비트맵 글꼴을 사용하는 자체 에디트 컨트롤은 편집기의 클라이언트 영역뿐만 아니라 대화상자에서 입력란으로도 쓰인다. 그리고 한글 낱자를 뽑아 오는 독특한 스타일의 콤보 박스라든가 문자표 리스트, 글쇠배열 편집 윈도우도 다 자체 개발한 custom 컨트롤이다.

꼭 그렇게까지 특이한 용도의 윈도우가 아니더라도.. 이걸 생각해 보자.
자체적인 키보드 포커스를 받으며, 화살표 key나 마우스 휠로 2차원적인 스크롤이 가능한 형태로 그림 같은 owner-draw 컨텐츠를 출력하는 컨트롤.
쉽게 말해 MFC로 치면 CScrollView에 해당하는 컨트롤이다. 특이할 것 없는 아주 기본적인 기능임에도 불구하고 기존 컨트롤의 서브클래싱만으로는 구현 가능하지 않다.

키보드 포커스를 받지 않으며 스크롤 같은 것도 없이 고정적인 owner draw 그림만을 화면에 달랑 그리는 거라면 쉽다. 기성 컨트롤인 STATIC 윈도우에다 owner draw 스타일을 줘서 WM_DRAWITEM 메시지를 처리하거나, 아예 서브클래싱을 해서 WM_PAINT 메시지를 통째로 가로채면 된다.

그러나 스스로 키보드와 마우스 입력을 처리하는 윈도우라면 독립된 형태로 자체 구현이 불가피하다. 여러 메시지들에 대한 동작을 인위로 고쳐야 하기 때문이다.
<날개셋> 편집기에서 화면 인쇄를 시켰을 때 화면 인쇄 결과를 출력하는 윈도우가 바로 이런 용도로 만들어진 custom control이다.

custom control을 MFC를 써서 개발한다면 응당 CWnd에서 파생된 새로운 클래스를 떠올리게 될 것이다. 이 C++ 클래스는 자신만의 윈도우 클래스 이름을 갖고 있을 것이고 자신을 운영체제에다 등록하는(RegisterClass 호출) 함수도 static 형태로 갖추고 있어야 할 것이다. (응용 프로그램이 실행 초기에 호출함)
그리고 대화상자 리소스에는 그 클래스 이름을 지정해 놓은 custom 컨트롤을 마음껏 생성해 놓는다.

그런데 대화상자의 컨트롤들은 내가 CWnd::Create를 호출하여 만드는 게 아니라 운영체제의 대화상자 관련 함수들이 CreateWindowEx에다가 윈도우 클래스 이름만 달랑 줘서 만들어진다. 이렇게 C++ 언어 체계의 통제를 받지 않고 밖에서 만들어진 HWND에다가 MFC 기반의 C++ 개체는 어떤 메커니즘을 통해 상호 연결시켜야 할까?

단순히 CWnd::FromHandle 함수 같은 걸로 임시 CWnd만 달랑 생성해서 연결하는 게 아니다. 이 custom 컨트롤은 구현체 클래스가 존재하니, 그냥 CWnd가 아니라 내가 만든 전용 CWnd 파생 클래스가 정확히 연결돼야 한다. 그렇게 해야 응용 프로그램 해당 윈도우끼리는 type-safe하지 않고 불편한 메시지를 쓸 필요 없이 C++ 멤버 함수/변수에 직통으로 가능해지니 좋다.

연결하는 방법은 크게 두 가지가 있다. 먼저, C++ 클래스의 생명 주기를 윈도우 자체의 생명 주기와 일치시키는 것이다. 만드는 윈도우 클래스에다가 윈도우 프로시저를 아래와 같이.. MFC의 AfxWndProc와 거의 똑같은 형태로 작성해 준다.

LRESULT CALLBACK CHeapCtrl::_MyWndProc(HWND hWnd, UINT msg, WPARAM w, LPARAM l)
{
    CWnd *pWnd = CWnd::FromHandlePermanent(hWnd);
    if (pWnd==NULL) { pWnd=new CHeapCtrl; pWnd->Attach(hWnd); }
    return AfxCallWndProc(pWnd, hWnd, msg, w, l);
}

이 개체는 언제나 new 연산자를 통해 heap에만 생성된다. 그러므로 PostNcDestroy 함수를 다음과 같이 구현하여 윈도우와 함께 C++ 개체도 같이 소멸되게 해 줘야 한다.

void CHeapCtrl::PostNcDestroy()
{
    delete this;
}

또한 대화상자 내부에서는 CHeapCtrl을 언제나 포인터를 통해 접근해야 한다.

CHeapCtrl *p = DYNAMIC_DOWNCAST(CHeapCtrl, GetDlgItem(IDC_MYCONTROL));

둘째 방법은.. 대화상자이니까 가능한 더 간단한 방법이다. 대화상자 클래스에다가 해당 컨트롤 클래스의 개체를 선언한다. 윈도우 클래스를 등록할 때 WNDCLASS 구조체에다가 윈도우 프로시저는 그냥 쿨하게 DefWindowProc이라고 주면 된다.

CStackCtrl m_wndCustomCtrl;

그리고 윈도우와 C++ 개체를 그냥 이렇게 컨트롤 변수로 연결해 버린다.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_MYCONTROL, m_wndCustomCtrl);
}

이 경우, CStackCtrl은 스택, 아니 최소한 대화상자 클래스와 동일한 메모리에 생성된다. 그래서 개체를 포인터를 거치지 않고 더 간편하게 접근할 수 있을 뿐만 아니라, 대화상자 같은 윈도우 개체가 사라진 뒤에도 C++ 객체가 갖고 있던 내부 정보에 접근을 할 수 있다. PostNcDestroy 처리가 필요하지 않음은 두 말할 나위도 없고. 여러 모로 더 속 편하다.

윈도우 클래스상으로 이 윈도우는 프로시저가 아무 특수한 일도 하지 않는 DefWindowProc로 지정되어 있지만, DDX_Control 함수는 윈도우 프로시저를 서브클래싱하고 저 윈도우의 핸들을 우리가 준 CStackCtrl에다가 연결해 준다. 그래서 각종 메시지가 발생했을 때 CStackCtrl의 메시지 맵에 등록된 핸들러 함수가 호출되는 것이다.

윈도우 클래스와 MFC 클래스를 연결한 건 시작일 뿐이고 본격적인 코딩은 지금부터이다. 그야말로 산 넘어 산이다.
WM_PAINT와 필요하다면 WM_SIZE를 처리해야 할 것이고, 키보드 포커스를 받는 물건으로 계획을 했으니 WM_GETDLGCODE를 잡아서 스크롤을 위해 최소한 DLGC_WANTARROWS 정도는 되돌려야 할 것이다. 다른 단축키 같은 걸 추가로 인식하려면 DLGC_WANTCHARS도 필요할 테고.

스크롤이 되는 윈도우라면 요즘은 마우스 휠 인식은 필수다. WM_MOUSEWHEEL을 처리해야 한다. SystemParametersInfo(SPI_GETWHEELSCROLLLINES, ...) 을 호출하여 마우스 휠의 움직임 단위를 감지하여 동작할 필요가 있다. 특히 n줄이냐, 아예 페이지 단위냐(WHEEL_PAGESCROLL)를 잘 판단해야 한다.

만드는 컨트롤에 확대/축소 배율이 존재한다면 Ctrl+휠로 배율 조절을 시키는 게 요즘 트렌드다.
또한, 전통적인 세로 스크롤용 마우스 휠뿐만이 아니라 가로 마우스 휠 WM_MOUSEHWHEEL (wheel 앞에 H 추가)이라는 게 있다는 것도 알아 두면 좋다. 마우스 현물보다는 손가락 제스처를 이용한 스크롤 기능이 있는 노트북 터치패드로 생성되는 듯하다.

어디 그 뿐이랴? 표준 인터페이스에는 아예 마우스 휠을 눌러서 구동하는 '자동 스크롤' 모드도 있다. WM_MBUTTONDOWN(마우스 가운데 버튼) 되시겠다. 이건 아까와는 반대로 마우스 실물이 아닌 노트북 터치패드로는 구경하기 힘든 모드이다.

.마우스로 화면을 끌어서 스크롤이 되게 하려면 WM_LBUTTONDOWN, WM_MOUSEMOVE 같은 메시지들을 처리하면 될 것이고.
아, 이런 것보다 더 중요하면서도 스크롤 윈도우에서 상당히 번거로운 작업이 하나 있는데 그건 바로 WM_HSCROLL 및 WM_VSCROLL 메시지이다. 우리의 편견과는 달리,처음에 SetScrollInfo 함수 하나로 전체 스크롤 크기와 영역만 지정해 준다고 해서 그 다음부터 모든 스크롤 처리가 자동으로 되는 게 아니기 때문이다.

스크롤 메시지는 사용자가 스크롤 바의 화살표(한 칸씩)를 눌렀을 때, 스크롤 바를 드래그하고 있을 때, 스크롤 바 옆을 눌렀을 때(한 페이지 씩) 등등의 상황별로 서로 다른 정보가 담긴 채로 전달된다. 그리고 이때 실제로 화면을 얼마만치 스크롤시키고 어떤 처리를 할지는 전적으로 해당 응용 프로그램에 달려 있다. 운영체제가 자동으로 해 주는 일은 없다. 이걸 처리하지 않으면 사용자가 스크롤 바를 눌러도 다른 반응이 발생하지 않는다.

바로 이런 특성 때문에 사용자가 스크롤 바를 끌고 있는 동안 화면이 바로 갱신될지, 혹은 곧장은 아니고 스크롤 바의 드래그가 끝난 뒤에야 화면을 갱신할지 같은 것도 응용 프로그램마다 달리 동작할 수 있다. 물론 반응성이 좋은 프로그램이라면 어지간하면 즉각 화면이 갱신되는 게 좋겠지만 말이다.

그리고 요즘은 화면 캡처 유틸리티들이 스크롤 캡처를 지원하는데, 이 역시 생각보다 꼼수를 써서 구현돼 있다. 화면에다 별도의 표식을 그려 넣은 뒤, 캡처 대상 윈도우에다 스크롤 메시지를 보내고 그 표시가 얼마나 이동했는지를 수동으로 점검한다. 위와 같은 높은 자유도로 인해, 저렇게 하지 않으면 그 스크롤 분량을 정량적으로 알아낼 수 없기 때문이다. (스크롤 바가 이동한 양과 그에 따라 지금 화면이 실제로 이동한 픽셀수 사이의 인과관계)

화면이 스크롤되면 ScrollWindowEx 함수를 호출해서 이미 그려진 화면은 운영체제가 제공하는 스크롤 기능으로 넘기고 새로 칠해져야 하는 최소한의 부분만 새로 칠하는 '평범한 프로그램'이라면.. 저 꼼수가 통한다.
그러나 스크롤 될 때마다 화면을 몽땅 지우고 새로 그리는 프로그램이라면, 저 표식도 지워지기 때문에 스크롤 캡처를 할 수 없게 된다.

이상이다. 윈도우의 스크롤 기능까지 얘기하다 보니 말이 또 길어졌다. 여기에도 뭔가 정량적인 동작 패턴이 분명 있는 듯한데, 그것만 추려내기는 쉽지 않아 보인다.
공통된 기능을 운영체제가 API 함수로든, 공용 컨트롤 윈도우로든 뭘로든 좀 제공을 하지 않는다면 역시나 프로그래머들이 비슷한 기능을 여전히 자체적으로 중복 구현할 수밖에 없을 것이다.

Posted by 사무엘

2014/07/22 08:36 2014/07/22 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/987

오래 전, 본인은 PE 방식이라고 불리는 32비트 Windows 실행 파일의 내부 구조에 대해 처음 알아 가던 시절에 굉장히 신기해한 사실이 하나 있었다. 그건 바로 파일 내부에 자신이 호출하는 API 함수의 이름이 다 나와 있다는 점이었다. 그러면 이 프로그램이 대충 무슨 기능을 활용하며 만들어졌는지도 얼추 분석이 가능해질 텐데? 16비트 바이너리에는 이런 정보가 존재한 적이 없었다. (오히려 EXE가 윈도우 프로시저 같은 콜백 함수 이름을 노출하고 있었음)

static library가 그러한 것처럼 DLL도 프로그래머가 작성한 클래스/함수가 이름이 그대로 외부로 노출된다. 그 이름을 GetProcAddress에다 전달하면 이름에 해당하는 함수 주소를 얻을 수 있다.

그러나 DLL이 제공하는 심벌들은 이름뿐만 아니라 ordinal이라고 불리는 번호도 제각각 다르게 부여받는다. 그렇기 때문에 ordinal로 주소를 얻는 것 역시 가능하다.
이 ordinal은 index나 number이 아니라 ID에 가까운 개념이다. 반드시 0부터 N까지 조밀하게 분포해 있어야 할 필요가 없으며 1000부터 시작해도 되고 중간에 빈 번호가 있어도 괜찮다.

단, 범위는 16비트로 한정이다. GetProcAddress 함수는 인자의 정수값이 16비트보다 크면 포인터로 간주하여 문자열 검색을 하며, 그보다 작은 값이면 ordinal로 간주하여 숫자 검색을 하는 방식으로 동작한다.
다시 말해 Windows의 DLL은 구조적으로 65536개 이상의 심벌을 export할 수는 없다. 물론 그렇다 해도 이것은 현실적으로 아무런 한계가 없는 것이나 마찬가지다.

16비트 시절에는 DLL의 심벌 탐색이 이름이 아닌 오로지 ordinal 방식만 지원되었던 모양이다. (그럼 GetProcAddress 함수도 인자가 PCSTR이 아니라 그냥 UINT였나?)
문자열을 비교하는 것보다는 숫자를 비교하는 게 속도도 더 빠르고 공간도 더 적게 차지하니 좋다. 그러나 ordinal 방식은 두 가지 단점이 있는데, 먼저 보안이 좀 더 안 좋으며, 그리고 ordinal 관리가 매우 까다롭다는 점이다.

보안 이슈는 쉽게 비유하자면 이렇다.
GetProcAddress("My_unique_function_name")은 내가 직접 만들지 않은 DLL 에서는 성공할 확률이 거의 없다. 그 반면, GetProcAddress((PCSTR)5)는 함수깨나 있다 싶은 아무 어중이떠중이 DLL에서도 어지간해서는 성공하게 된다.

즉, 엉뚱한 DLL을 잘못 불러왔을 때, 이후 동작이 안전하고 깔끔하게 실패하는 게 아니라 그 상황을 사전 감지를 못 하고 나중에 crash로 도질 가능성이 높다는 뜻이다.
물론, 여기서 보안이라는 건 프로그램 실행과 관련된 보안이다. ReadFile, CreateWindow 이라는 함수 이름 대신 #35, #107 식의 암호 같은 ordinal은 프로그램의 역공학을 어렵게 하는 보안(?)은 더 뛰어날 수도 있으니 말이다.

ordinal 관리 문제는 생각보다 더 까다로운 문제이다.
어떤 DLL이 개발이 한창 진행 중이어서 수시로 함수가 추가되거나 삭제된다고 생각해 보자. 그렇더라도 한번 번호가 부여된 함수는 번호가 절대 고정불변이어야만 그 DLL을 사용하는 프로그램과의 하위 호환성이 보장될 수 있다.

같은 함수라도 DLL의 다음 버전에서 ordinal이 달라져 버리면 기존 프로그램은 그 DLL을 사용할 수 없게 된다. 그런데 수백, 수천 개의 ordinal간에 결번이 생기고 영역이 추가 할당되는 것, 과연 번호 관리가 그렇게 호락호락 수월하게 가능할까?

이런 이유로 인해 32비트 이래로 DLL 심벌은 ordinal이 아닌 문자열로 import/export하는 게 관행이 되었다. 16비트 시절에는 DLL을 하나 만들려면 DEF 파일을 무조건 반드시 만들어야 했고 export하는 심벌에 대한 ordinal을 수동으로 기입해야 했다.
그러나 32비트부터는 export하는 심벌만 쭉 기입해 주면, ordinal은 그냥 이름들의 ABC순으로 0부터 N까지 자동으로 생성된다. 별로 중요하지 않은 정보가 됐기 때문이다.

그러나 오늘날에도 ordinal이 전혀 불필요하고 쓸데없느냐 하면 그렇지는 않다.
딱히 컴포넌트화를 지향하지 않고 내가 만드는 프로그램에서나 내부적으로 몰래 쓰는 소형 private DLL이라면, export하는 함수의 이름이 전혀 중요하지 않을 테니 그냥 이름을 노출할 필요도 없이 ordinal 직통을 쓰면 된다. 하는 일이 붙박이로 정해져 있고 앞으로 프로토타입이 바뀔 일이 절대로 없는 물건이라면 금상첨화. 훅 프로시저 DLL 같은 게 좋은 예 되겠다.

혹은, 심벌 개수가 수천~수만 개로 너무 많은 대형 DLL의 경우, 로딩 시간을 조금이라도 단축하기 위해서 의도적으로 이름 대신 ordinal 기반 로딩 방식을 고집하기도 한다.
당장 MFC 라이브러리, 그리고 MS Office가 내부적으로 사용하는 공용 라이브러리인 mso.dll도 전부 ordinal 기반이다. MFC를 DLL 링크한 프로그램이라고 해서 export 섹션에 CWnd, CWinApp 이런 클래스 심벌들이 주룩 노출돼 있는 거 아니다.

심벌을 이름이 아닌 ordinal로 식별하게 DLL과 import library를 만들려면 빌드 시에 DEF 파일을 만들어서 심벌에 대한 특성과 ordinal 번호를 수동으로 지정해 줘야 한다.
그런데 C가 아닌 C++ 스타일의 클래스나 함수를 ordinal로 지정하는 법은 잘 모르겠다. 비주얼 C++ 스타일로 복잡하게 decorate된 명칭들을 일일이 다 열거하면서 @번호 NONAME 속성을 다 줬으려나? 그것도 보통일이 아닐 텐데.

Posted by 사무엘

2014/06/12 08:33 2014/06/12 08:33
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/973

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2021/12   »
      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:
1699267
Today:
388
Yesterday:
537