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

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

1. 재부팅 이벤트

본인은 지금까지 Windows가 시스템 종료/재시작/로그아웃 될 때.. 메시지에 즉각 응답하면서 정상적으로 돌아가는 프로그램이라면 종료 처리도 당연히 정상적으로 되는 줄로 알고 있었다.
사용자가 [X] 버튼이나 Alt+F4를 눌러서 종료할 때와 완전히 동일하게 말이다. WM_CLOSE에 이어 WM_DESTROY, WM_NCDESTROY가 날아오고, WM_QUIT이 도달하고, message loop이 종료되고, MFC 프로그램으로 치면 ExitInstance와 CWinApp 소멸자가 호출되고 말이다.

왜냐하면 시스템이 종료될 때 발생하는 현상이 언뜻 보기에 정상적인 종료 과정과 동일했기 때문이다. 저장되지 않은 문서가 있는 프로그램에서는 문서를 저장할지 확인 질문이 뜨고, 운영체제는 그 프로그램 때문에 시스템 종료를 못 하고 있다고 통보를 한다. 좋게 WM_CLOSE 메시지를 보내는 걸로 반응을 안 하는 프로그램에 대해서만 운영체제가 TerminateProcess 같은 강제 종료 메커니즘을 동원할 것이다.

그런데 알고 보니 그게 아니었다.
정상적인 종료라면 ExitInstance 부분이 실행되어야 할 것이고 프로그램 설정들을 레지스트리에다 저장하는 부분도(본인이 구현한..) 실행돼야 할 텐데, 내 프로그램이 실행돼 있는 채로 시스템 종료를 하고 나니까 이전 설정이 저장되어 있지 않았다.

꼭 필요하다면 WM_ENDSESSION 메시지에서 사실상 WM_DESTROY 내지 ExitInstance와 다를 바 없는 cleanup 작업을 해야 할 듯했다. 뭐, 날개셋 한글 입력기에서 이 부분이 반영되어 수정돼야 할 건 딱히 없었지만 아무튼 이건 내 직관과는 다른 부분이었다.

시스템이 종료될 때 발생하는 일은 딱히 디버거를 붙여서 테스트 가능하지 않다. =_=;; 로그 파일만 기록하게 해야 하고, 매번 운영체제를 재시작해야 하니 가상 머신을 동원한다 해도 몹시 불편하다. 꽤 오래 전 일이 됐다만, 날개셋 외부 모듈이 특정 운영체제와 특정 상황에서 시스템 종료 중에 운영체제 시스템 프로세스 안에서 죽는 문제가 발생해서 디버깅을 한 적이 있었다. 몹시 골치 아팠었던 걸로 기억한다.

2. 파일의 동등성 비교

파일 이름을 나타내는 어떤 문자열이 주어졌을 때,

  • 상대 경로(현재의 current directory를 기준) vs 절대 경로
  • 과거 Windows 9x 시절이라면 ~1 같은 게 붙은 8.3 짧은 이름 vs 원래의 긴 이름
  • 대소문자 (Windows는 대소문자 구분이 없으므로. 그런데 이것도 A~Z 26자만 기계적으로 되나? 언어별로 다른 문자의 대소문자는?)
  • 그리고 옵션으로는 정규 DLL search path와 환경 변수

이런 것들을 몽땅 다~ 감안해서 다음과 비스무리한 일을 하는 가벼운 API가 좀 있으면 좋겠다. 보다시피 동일한 파일을 표현하는 방법이 굉장히 다양하기 때문이다.

  • 두 개의 파일 명칭이 서로 동일한 파일인지 여부를 판단. 당연히 파일을 직접 open/load하지 않고 말이다.
  • 그 파일의 대소문자 원형을 유지한 절대 경로. 일명 '정규화된' 이름을 되돌린다. 이게 동일한 파일은 물리적으로, 절대적으로 동일한 파일임이 물론 보장된다.
    참고로 Windows API 중에서 얼추 비슷한 일을 한다고 여겨지는 GetFullPathName이나 GetModuleFileName 함수는 절대 경로 말고 파일의 대소문자 원형 복원이 제대로 되지 않는다.
  • 혹은 그 파일의 시작 지점이 디스크에서 물리적으로 어디에 있는지를 나타내는 64비트 정수 식별자를 구한다. 포인터가 아니지만 파일의 동등성 여부 판단은 가능한 값이다. 정수를 넘어 UUID급이어도 무방함.

본인은 비슷한 목적을 수행하기 위해, 그냥 두 파일을 CreateFileMapping으로 열어 보고 리턴된 주소가 동일하면 동일한 파일로 간주하게 한 적이 있었다. 핸들 말고 MapViewOfFile 말이다. 본질적으로 동일한 파일이라면 운영체제가 알아서 같은 주소에다 매핑을 하고 레퍼런스 카운트만 증가시킬 테니까..
Windows 9x에서는 주소가 시스템 전체 차원에서 동일하겠지만 NT 계열에서는 한 프로세스 내부에서만 동일할 것이다.

하지만 내가 필요한 건 파일의 내용이 아니라 그냥 두 파일의 동등성뿐인데 이렇게 매핑을 하는 건 overkill 삽질 같다는 인상을 지울 수 없었다.
비슷한 예로, LoadLibrary도 같은 파일에 대해서는 같은 리턴값이 돌아온다. HMODULE도 오늘날은 핸들이 아니라 메모리 map된 주소이니까.. 다만, 오버헤드를 줄인답시고 LoadLibraryEx + LOAD_LIBRARY_AS_DATAFILE 이렇게 열어서는 안 된다. 그러면 로딩 방식이 크게 달라지더라.

3. JNI에서 문자열 처리하기

Java 언어는 JNI라고 해서 자기네 바이트코드 가상 머신이 아닌 C/C++ 네이티브 코드를 호출하는 통로를 제공한다.
프로그램 자체를 C/C++로 짜던 시절에는 극한의 성능을 짜내야 하는 부분에 어셈블리어를 집어넣는 게 관행이었는데.. 이제는 일반적인 코딩은 garbage collector까지 있는 상위 계층에서 수행하고, 극한의 성능을 짜내야 하는 부분에서만 C/C++ 코드를 호출한다는 게 흥미롭다.

JNI는 그냥 언어 스펙에 가까운 광범위한 물건이다. Windows 환경에서는 그냥 Visual C++로 빌드한 DLL이 export하는 함수를 그대로 연결할 수도 있다. 물론 그 DLL을 빌드하기 위해서는 Java SDK에서 제공하는 jni 인터페이스 헤더와 static 라이브러리를 사용해야 한다.
한편, 안드로이드 앱 개발에서 쓰이는 NDK는 JNI 스펙을 기반으로 자체적인 C++ 컴파일러까지 갖춘 네이티브 코드 빌드 도구이다.

Java의 문자열은 JNI에서는 jstring이라고 내부 구조를 알 수 없는 자료형의 포인터 형태로 전달된다. C++에서는 UTF-8과 UTF-16 중 편한 형태로 바꿔서 참조 가능하다.
UTF-8로 열람하려면 JNIEnv::GetStringUTFChars를 호출하면 된다. 길이를 알아 오려면 GetStringUTFLength부터 호출한다. 전해받은 문자열 포인터는 ReleaseStringUTFChars로 해제한다.

그 반면, UTF-16 형태로 열람하려면 위의 함수 명칭에서 UTF를 빼면 된다. GetStringChars, GetStringLength, ReleaseStringChars의 순이다. Java가 내부적으로 문자를 2바이트 단위로 처리하기 때문에 이들이 주로 취급하는 자료형은 jchar*이다. 그러니 얘는 char16_t 자료형과 호환된다고 간주해도 좋다. 참고로 wchar_t는 NDK 컴파일러의 경우 4바이트로 처리되더라.

UTF-16이나 UTF-8이나 다 UTF이긴 마찬가지인데, Java는 변별 요소인 8을 생략하고 함수 이름을 왜 저렇게 지었나 개인적으로 의구심이 든다. 물론 GetStringChars는 Java가 내부적으로 문자열을 원래부터 2바이트 단위로 처리하다 보니 우연히 UTF-16과 대응하게 됐을 뿐, 대놓고 UTF-16을 표방했던 건 아닐 것이다. 뭐, 이제 와서 그 체계를 바꾸는 건 불가능하고 "자바 문자열 = 2바이트 단위"는 완전히 고정되고 정착했지만 말이다.

또한 GetStringChars는 GetStringUTFChars와 달리 굉장히 치명적으로 불편한 단점이 하나 있다. 바로.. 변환된 문자열이 NULL-terminated라는 보장이 없다는 것이다!
그래서 본인은 이 포인터를 사용할 때 메모리를 n+1글자만치 또 할당해서 null문자를 추가해 주는 매우 번거로운 두벌일을 하고, 아예 클래스를 이렇게 따로 만들어야 했다. 좀 개선의 여지가 없으려나 모르겠다.

class CJstrToString16 {
    JNIEnv *_ev;
    jstring _jstr;
    const jchar *_ret;
    char16_t *_arr;
public:
    CJstrToString16(JNIEnv *ev, jstring js): _ev(ev), _jstr(js) {
        jsize n = ev->GetStringLength(js);
        _ret = ev->GetStringChars(js, NULL);
        _arr = new char16_t[n+1];
        memcpy(_arr, _ret, n*sizeof(char16_t));
        _arr[n] = 0; //고작 요거 하나 때문에..
    }
    ~CJstrToString16() {
        ev->ReleaseStringChars(_jstr, _ret);
        delete[] _arr;
    }
    operator const char16_t*() const { return _arr; }
};

4. Visual C++의 STL

C++은 타 프로그래밍 언어들과 달리, 심지어 전신인 C와도 달리, 언어가 개발되고 나서 자신의 특성을 잘 살린 라이브러리가 언어 차원에서 붙박이로 곧장 제정되지 않았던 모양이다. 그래서 각 컴파일러들이 중구난방으로 파편화된 형태로 라이브러리를 제공해 오다가.. 표준화라는 게 1990년대 말이 돼서야 논의되기 시작했다.

템플릿이 추가되어 C++에서도 제네릭, 메타프로그래밍이라는 게 가능해진 뒤부터 말이다. 처음에는 자료구조 컨테이너 위주로 STL이라는 이름이 붙었다가 나중에는 그냥 C++ library가 된 걸로 본인은 알고 있다.

Windows용으로 가장 대중적인 C++ 컴파일러야 두 말할 나위 없이 MS Visual C++이다. 얘는 거의 20여 년 전 6.0 시절부터 P.J. Plauger라는 사람이 구현한 C++ 라이브러리를 제공해 왔다. C 라이브러리와 달리 C++ 라이브러리는 소스가 비교도 안 될 정도로 복잡하고 난해하다는 것(암호 같은 템플릿 인자들..=_=), 그리고 저렇게 마소 직원이 아닌 개인 이름이 붙어 있다는 게 인상적이었다. 2000년대 초까지만 해도 휴렛-패커드라는 회사명도 주석에 기재돼 있었다.

P.J. Plauger는 현재는 Dinkumware라고 C++ 라이브러리만 전문적으로 관리하고 라이선스 하는 회사를 설립해 있다. 나이도 생각보다 지긋한 듯..
그런데 이런 세계적인 제품에 들어가는 라이브러리가.. 성능이 의외로 시원찮은가 보다. Visual C++이 제공하는 컨테이너 클래스가 유난히도 느리다고 까이는 걸 여러 사이트에서 봐 왔다.

최근에는 본인 직장의 상사마저도 같은 말씀을 하시기에 "헐~!" 했다. 업무상 필요해서 string, set, map 등을 써서 수십, 수백 MB에 달하는 문자열을 분석하는 프로그램을 돌렸는데, 자료 용량이 커질수록 속도가 급격히 느려져서 자료구조를 직접 새로 짜야 할 판이라고 한다.

난 개인적으로는 C++ 라이브러리를 거의 사용하지 않고, 더구나 그걸로 그 정도까지 대용량 작업도 해 보지 않아서 잘 모르겠다. 그 날고 기는 전문가가 만든 코드에 설마 그런 결함이 있으려나? 아니면 컴파일러의 최적화 문제인지?

글쎄.. 이런 게 있을 수는 있다. MFC의 CString은 그냥 포인터와 크기가 동일하며 값으로 전할 때의 reference counting도 처리한다. 그러나 std::string은 자주 쓰이는 짧은 문자열을 번거로운 heap 메모리 할당 없이 빠르게 취급하기 위한 배열까지 내부에 포함하고 있다. 이런 특성을 모르고 std::string도 함수에다 매번 value로 전달하면 성능에 악영향을 줄 수밖에 없다.

그런 식으로 임시 객체가 쓸데없이 생겼다가 사라지는 구조적인 비효율이 C++ 라이브러리에 좀 있는 걸로 들었다. R-value 참조자 &&가 도입된 것도 vector의 내부 처리에서 그런 삽질을 예방하는 근거를 언어 차원에서 마련하기 위해서라지 않는가? 그리고 Visual C++이 그런 비효율을 보정하는 성능이 좀 시원찮다거나 한 것 같다. 전부 다 그냥 추측일 뿐이다.

그러고 보니 cout<<"Hello world"가 printf("Hello world")보다 코드 오버헤드가 작아지는 날이 과연 올지 모르겠다. 이것도 그냥 떡밥인 건지..?? =_=;;

Posted by 사무엘

2019/04/09 08:32 2019/04/09 08:32
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1606

* 2014년에 썼던 글을 보완하여 다시 올린다.

옛날에 도스 시절에는 일명 '외부 명령'이라 하여 별도의 프로그램 형태로 존재하는 명령들이 있었다. format.com, diskcopy.exe 같은 것들.
이것들은 자기가 소속된 도스 버전을 가려서 동작했다. 가령, MS 도스 5.0이 설치된 컴퓨터에다 도스 6.x에 존재하는 새로운 유틸리티를 복사해 와서 실행하면, 실행에 필요한 파일들이 다 있다 하더라도 '도스 버전이 다릅니다'라는 에러 메시지와 함께 프로그램이 그냥 실행되지 않았다. 이것은 운영체제의 버전을 가려 가며 실행하는 프로그램을 본인이 난생 처음으로 접한 사례였다.

Windows에도 자신의 버전을 알려 주는 API가 응당 존재한다. 하지만 이건 지금 구동 중인 운영체제가 무엇인지를 알려 주는 편의 기능을 구현할 때나 사용할 만한 기능이다. 일반적인 프로그램이라면 About 대화상자 같은 데서 말이다.
만약 프로그램이 운영체제의 버전을 가려 가며 실행해야 한다면, 단순히 운영체제의 버전을 갖고 판단하는 건 썩 좋은 방법이 아니다. 내가 실제로 사용하고자 하는 기능을 요청해 보고(CoCreateInstance, LoadLibrary/GetProcAddress 등), 그 요청의 성공 여부에 따라 실행 여부를 결정하는 게 바람직하다.

뭐, 지금은 아무 의미가 없는 예가 돼 버렸다만,
가령 내 프로그램이 유니코드 API를 사용하기 때문에 Windows 9x에서는 실행을 거부해야 한다고 치자.
그렇다면 CreateWindowExW건 RegisterClassW건 유니코드 API를 실제로 호출해 본 뒤, 그게 실패하고 GetLastError()==ERROR_CALL_NOT_IMPLEMENTED가 돌아올 때 실행을 거부하면 된다. 운영체제의 외형보다는 그 운영체제의 실제 실행 결과를 보고 판단하는 게 낫다는 게 바로 이런 의미이다.

그런 것도 다 필요 없고 운영체제의 버전 숫자를 정말로 정확하게 알아 와야 한다면,
그 경우를 위해 태초에 GetVersion()이라는 간단한 함수가 있었다. 얘는 버전과 관련된 여러가지 정보들을 비트 자릿수별로 묶은 32비트 정수를 되돌렸다.

그 정보의 의미를 C언어의 비트필드 구조체로 나타내 보면 대충 다음과 같다. 주석으로 표시된 숫자는 윈도 7 기준으로 반환되는 값들이다.
(최신 Windows 10 기준의 반환값을 소개하지 않은 이유는 후술하도록 하겠다)

union WINVERSION {
    DWORD dwValue;
    struct {
        UINT nMajorVer: 8; //6
        UINT nMinorVer: 8; //1
        UINT nBuildNumber: 15; //7601
        UINT bWin9xOrWin32s: 1; //0
    };
};

WINVERSION os;
os.dwValue = ::GetVersion();

이 함수는 아무 매개변수도 필요하지 않으며, 리턴값도 DWORD 달랑 하나이니 미치도록 가볍고 사용하기 편하다. Windows 9x와 NT 계열이 공존하던 옛날에, 지금 운영체제가 (1) NT 계열인지를 알고 싶다면 GetVersion()&0x80000000 (최상위 비트)만 하면 OK였다.
그 뒤, NT 3.x인지 4.0인지, 9x 계열의 경우 95인지 98인지 ME인지 같은 건 (2) major와 minor 번호를 보고 판별하면 됐다. (3) 빌드 번호는... 딱히 막 중요한 정보는 아닌 듯하다.

그러나 이 함수는 문제점과 한계도 보였다. 한눈에 봐도 각 비트로부터 의미 있는 정보를 추출하는 게 매우 지저분하고 번거로웠다. HIWORD, LOBYTE 삽질이 싫다면, 저런 비트필드 구조체는 프로그래머가 재량껏 알아서 만들어야 했으며, 응용 프로그램이 이 정보를 잘못 취급하는 경우도 많았다.

비교할 필요가 없는 필드까지 다 비교를 해 버리는 바람에, Windows 95 이상에서 모두 동작할 수 있는 프로그램이 Windows 95에서“만” 동작하게 고정돼 버리기도 했다. 혹은 Windows NT 4.0이 NT 3.51보다 낮은 버전으로 취급되는 촌극도 벌어졌다. (리틀 엔디언 기준으로 저 구조체를 보면, minor 버전이 major 버전보다 더 높은 자릿수에 놓여 있음)

더구나 운영체제의 정체성을 나타내는 정보는 단순히 버전 번호와 빌드 번호 이상으로 더욱 복잡해져 왔다. NT 계열의 경우 당장 서비스 팩이 있고, 이게 무슨 에디션인지도(홈? 서버? 워크스테이션? 등) 알 필요가 있는데 단순히 숫자 하나만 달랑 되돌리는 함수로는 그런 걸 알려 줄 수가 없었다.

이런 문제를 해결하기 위해 Windows 95 내지 NT 3.5에서는 OSVERSIONINFO라는 구조체를 인자로 받는 GetVersionEx라는 함수가 추가되었다. major, minor 버전 번호와 빌드 번호, 운영체제 계열이 모두 독립된 구조체 멤버로 독립하였으며, (4) 서비스 팩 내역도 완전한 문자열 형태로 되돌려 주니 버전 정보를 다루기가 편해졌다.

이 구조체는 맨 앞에 자신의 크기를 써 주게 돼 있으며, 덕분에 추후 확장이 가능한 형태이다.
Windows 2000부터는 OSVERSIONINFOEX 구조체가 추가됐다. 확장된 구조체는 서비스 팩의 번호조차도 major와 minor 꼴로 받을 수 있으며, (5) 같은 NT 계열 중에서도 클라이언트 라인과 서버 라인을 구분할 수 있다(wProductType==VER_NT_WORKSTATION / VER_NT_SERVER). Windows XP와 Server 2003은 버전 번호가 5.1과 5.2로 서로 달랐지만, 후대 버전부터는 버전 번호는 동일하고 이걸로 구분을 해야 한다. (Vista / Server 2008, 10 / Server 2016 같은..)

그리고 클라이언트 라인은 XP 이래로 오늘날의 10까지 (6) home과 pro 에디션 구분이 거의 관행이 돼 있는데.. 이건 wSuiteMask 멤버의 비트 플래그 VER_SUITE_PERSONAL (0x200)의 존재 여부로 판별 가능하다. 저 플래그가 존재하는 게 home 에디션이다.
VER_SUITE_* 다른 플래그들 중에는 Windows XP의 embedded 에디션, enterprise 에디션 같은 걸 나타내는 것들도 있으니 참고하면 된다.

요컨대 9x/NT 이후로도 클라이언트/서버, home/pro 같은 복잡한 구분이 계속 이어지는 것을 알 수 있다. 그래도 GetVersionEx 한 방이면 모든 정보를 얻을 수 있다.

이걸로 모든 이야기가 끝이 났으면 좋겠지만.. 아이고, 끝이 아니다. GetVersionEx 함수는 2010년대 이후로 마소의 정책상 사용이 더 권장되지 않는 deprecate 판정을 받고, 시간이 정지해 버렸다.
이 함수는 아무런 단서가 없는 환경에서는 Windows 8, 즉 버전 6.2보다 더 높은 값을 되돌리지 않는 샌드박스가 되었다. 실제로는 이 컴퓨터에 Windows 8.1이나 10이 돌아가고 있더라도 말이다. 이와 관련된 더 자세한 정보를 원한다면 다음 URL을 참고하시기 바란다.

이제 이 함수는 응용 프로그램에게 그 응용 프로그램보다 나중에 출시된 운영체제에 대한 정보는 주지 않기로 작정한 듯하다. GetVersionEx가 샌드박스 없이 실제 자기 버전을 되돌리는 조건은 다음과 같다.

  • 응용 프로그램의 manifest XML에(compatibility-application-supportedOS) 그 운영체제의 GUID가 등록되어 있다.
  • 혹은 응용 프로그램의 PE 헤더에 OS의 최소 요구 버전이 최신 운영체제의 버전으로 맞춰져 있다. Windows 8.1의 경우 6.3, Windows 10이라면 10.0이 되겠다.

운영체제와 함께 제공되는 메모장 같은 기본 프로그램들은 후자의 조치를 취한 상태이다. 이렇게 빌드된 프로그램에서는 GetVersionEx가 해당 버전을 정확하게 되돌린다. 하지만 이런 프로그램은 이전 버전 운영체제에서는 아예 전혀 동작하지 않으므로, 3rd-party 응용 프로그램이라면 이런 방법을 쓰기 곤란하다. 그러니 매니페스트 등록을 해야 한다.

물론 마소에서 2015년의 Windows 10부터는 기존 버전 번호 자체를 10.0으로 동결시켜 버리고 더 바꾸지 않기로 작정했다. 그러니 버전 번호 변경으로 인해 GUID를 또 등록하는 식의 혼란은 앞으로 더 없을 것이다.

운영체제의 버전의 절대값을 되돌리는 GetVersionEx 대신 마소에서 사용을 권장하는 함수는... 지금 운영체제의 버전이 응용 프로그램이 제시하는 버전보다 상대적으로 높은지 안 높은지 여부만을 되돌리는 VerifyVersionInfo 함수이다. 그리고 이걸 기반으로 IsWindows10OrGreater 같은 helper 함수들도 만들어져 있다. (VersionHelpers.h)

하지만 이 함수들도 내부적으로 GetVersionEx의 결과값을 기반으로 비교를 하는 것이기 때문에 앞서 언급한 샌드박스의 제약을 받는 건 마찬가지이다.

샌드박스 없이 운영체제의 정확한 버전을 얻어 오는 함수는 크게 두 군데에 있다.
먼저, 의외로 네트워크 API이다. 그렇다고 소켓 API 같은 건 아니고, Windows에서 독자적으로 제공하는 함수 중에 내 로컬 컴퓨터를 포함하여 원격 컴퓨터에 설치된 운영체제의 버전을 얻어 오는 함수가 있다. 대략 다음과 같이 코드를 작성하면 된다.

#include <LM.h>
#pragma comment(lib, "netapi32")

WKSTA_INFO_100 *p;
::NetWkstaGetInfo(NULL, 100, (LPBYTE *)&p);
printf("%d, %d\n", p->wki100_ver_major, p->wki100_ver_minor); //10, 0
::NetApiBufferFree(p);

저기 100은 수효를 나타내는 게 아니며 각각의 숫자들이 별개의 의미를 지님에도 불구하고, 상수 명칭이 존재하지 않아서 그냥 생으로 100을 넘겨 줘야 한다.
운영체제 버전 하나 좀 얻자고 웬 생뚱맞은 분야의 API를 써야 하는 것도 삽질스럽지만.. 저 함수를 통해서는 그냥 major와 minor 버전 번호만 얻을 수 있다. 서비스 팩이나 빌드 번호 같은 세부 정보는 얻을 수 없다.

저거 말고 다른 대안으로는.. ntdll.dll에 있는 native API인 RtlGetVersion을 써도 된다.
OSVERSIONINFO(EX)의 포인터를 받아들이고 정수값을 리턴하므로 prototype이 기존 GetVersionEx와 거의 동일하다.
단, native API 버전은 성공한 경우의 리턴값이 0이다. 리턴 타입이 BOOL이 아닌 셈이다.

얘는 Windows 8.1 내지 10 같은 요즘 운영체제에서는 잘 동작하는데, 과거의 Windows 2000에서는 GetVersionEx와 달리 서비스 팩 정보를 되돌리지 않았던 것으로 기억한다. 구형 OS에서는 오히려 기존 함수를 쓰는 게 더 낫다. 거 참..;;
Windows가 지난 20년 동안 운영체제의 버전과 제품 종류를 얻는 그 단순한 절차만 해도 얼마나 복잡하고 지저분해져 왔는지를 확인할 수 있다. 관련 여담을 몇 가지 더 남기는 것으로 글을 맺고자 한다.

  • OSVERSIONINFOEX는 C++ 상속 문법 같은 걸 이용해서 선언된 게 아닌 관계로, OSVERSIONINFO와는 언어 차원에서 아무런 연결 고리가 없다. GetVersionEx에다가 전달할 때는 OSVERSIONINFO*로 reinterpret_cast를 해 줘야 된다.
  • 과거 Windows XP에는 media center 에디션 내지 태블릿 PC 에디션 같은 바리에이션이 있었는데.. 이거 여부를 얻는 건 GetVersionEx가 아니라 GetSystemMetric라는 다소 생뚱맞은 함수에 있었다. SM_MEDIACENTER, SM_TABLETPC처럼 말이다 .
  • 끝으로, Windows 10부터는 (7) 릴리스 연-월을 나타내는 4자리 숫자가 사실상 버전 번호가 됐으니 이걸 표시해 줘야 할 것이다. 그런데 이건.. 본인이 아는 방법은 그냥 무식한 레지스트리 조회가 유일하며, 공식적인 API가 따로 있지 않다.;;;

Posted by 사무엘

2019/03/14 08:36 2019/03/14 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1596

오늘은 Windows라는 운영체제가 GUI 프로그래밍 용도로 제공하는 공용 컨트롤들 중의 하나인 리스트뷰(List-view) 컨트롤에 대해 자세히 알아보도록 하겠다. (이름에서 '뷰'는 종종 생략되기도 함)
결론부터 말하자면 얘는 정말 세심하게 설계된 다재다능한 요물이다. 동일한 규격을 가진 다수의 아이템들, 특히 그림과 글자가 같이 가미된 아이템을 표시하는 모든 방식과 가능성을 고려해서 만들어졌다. 그래서 정말 많은 기능들을 제공한다.

리스트뷰가 기존의 재래식 초간단 리스트박스와 다른 점은 다음과 같다.

  • 리스트뷰는 글자뿐만 아니라 곁들여진 그림도 태생적으로 같이 처리 가능하다. 리스트박스에서는 그림과 글자를 같이 표시하기 위해서 얄짤없이 owner-draw로 가야 했다.
  • 마우스의 동작이 다르다. 리스트박스는 내부를 왼쪽 버튼으로 아이템을 선택해서 드래그 하면 선택막대가 자동으로 쭉 바뀌며 스크롤도 된다. 하지만 리스트뷰는 그렇지 않다.
  • 키보드의 동작도 다르다. 아이템을 복수 선택할 때 리스트뷰는 Ctrl+화살표를 눌러서 포커스만 이동시키고 Ctrl+Space로 선택을 하지만 리스트박스는 Shift+F8과 space 같은 다른 글쇠를 사용한다. 리스트뷰는 F2를 눌러서 아이템의 이름을 바꾸는 기능도 있지만 리스트박스는 그렇지 않다.

아울러, 리스트뷰가 같이 추가된 공용 컨트롤인 트리뷰(Tree-view) 컨트롤과 다른 점은 다음과 같다.

  • 트리는 아이템 하나를 HTREEITEM이라는 별도의 자료형으로 식별하지만, 리스트는 그냥 인덱스 번호이다. 트리는 노드 포인터 기반의 이산적인 컨테이너를 쓰지만, 리스트는 내부적으로 배열과 유사한 컨테이너를 쓰는 듯하다.
  • 리스트는 아이템의 복수 선택이 가능하지만 트리는 그렇지 않다.
  • 트리는 리스트와 같은 다양한 view 모드가 존재하지 않는다.
  • 아이템의 텍스트를 진하게 표시하는 state 플래그가 트리에는 있지만 리스트에는 없다.

리스트박스와 위상이 비슷한 자매 컨트롤(?)은 콤보박스이다. 하지만 리스트뷰와 위상이 비슷한 자매 컨트롤은 트리뷰라고 할 수 있다.
왼쪽에 트리뷰, 오른쪽에 리스트뷰를 배치한 프로그램으로는 탐색기, 레지스트리 편집기, 시스템 정보 등 의외로 꽤 많다. 왼쪽에서 카테고리를 선택하면 오른쪽에서 세부 정보가 표시되는 것이다. 오죽했으면 Visual C++의 MFC 프로젝트 마법사에도 요런 형태의 프로그램을 만드는 템플릿이 제공될 정도이다.

옛날에는 리스트박스를 서브클래싱 해서 drag & drop을 구현하고, owner-draw와 item data를 이용해서 얼추 트리 계층 구조라든가 check list를 구현하고, 파일이나 디렉터리나 드라이브 목록을 채워 주는 리스트를 만드는 등.. 별별 짓을 다 했다. 그리고 Visual Basic 부류의 RAD 툴들은 그걸 미리 구현해 놓은 리스트를 컴포넌트 형태로 제공했었다. 하지만 리스트뷰와 트리뷰 공용 컨트롤이 등장하면서 리스트박스의 역할이 상당수 분담되었다.

Windows 탐색기의 보기 메뉴에서 보는 바와 같이 리스트뷰 컨트롤에는 다양한 보기 모드가 있다.

(1) 큰 아이콘
아이콘이 중심이고 이를 설명하는 주 텍스트가 아이콘의 하단 중앙에 찍힌다. 이걸로 끝. 아이콘의 크기는 무엇이 되어도 상관없지만 보통은 표준 아이콘 크기인 32*32 또는 그보다 약간 더 큰 48*48이 쓰인다.
탐색기에서 확대 배율 조정이 되는 대부분의 모드들은 이 모드에 속한다. 아이콘의 크기만 바꾸는 거니까.. (보통 아이콘, 큰 아이콘, 아주 큰 아이콘..) 또한 당장 바탕 화면에 표시된 아이콘들도 다 리스트뷰의 이 모드인 것을 알 수 있다.

사용자 삽입 이미지

(2) 작은 아이콘
글자의 크기와 대등한 크기인 작은 아이콘이 쓰이며, 아이콘의 아래가 아니라 오른쪽에 주 텍스트가 나란히 찍힌다.

사용자 삽입 이미지

(3) 목록
아이템 하나가 표시된 모습이 작은 아이콘 모드와 완전히 동일하다. 그렇기 때문에 '작은 아이콘'과 차이가 무엇인지 언뜻 봐서는 구분하기 어렵다. 하지만 작은 아이콘(+ 큰 아이콘도 포함)에서는, 아이템을 드래그 해서 화면의 아무 위치로나 옮길 수가 있는 반면, 목록 모드는 그렇지 않다. i째 아이템은 현재의 스크롤 위치 기준으로 반드시 그에 상응하는 위치에 있어야 하며, 아무 위치로나 옮길 수 없다.

사용자 삽입 이미지

(4) 자세히(일명 report view)
한 줄에 아이템이 오로지 하나만 찍힌다. 작은 아이콘, 주 텍스트, 그 다음으로 n개의 부 텍스트가 마치 표처럼 일목요연하게 표시된다. 즉, 이 모드는 부 텍스트를 표 형태로 모두 볼 수 있는 유일한 모드이며, 상단에 헤더 컨트롤이 등장해서 쓰이는 유일한 모드이기도 하다.

사용자 삽입 이미지

사실, 헤더 컨트롤만 별도로 따로 생성할 수도 있다. 얘만으로도 각종 메시지 스펙이 공개돼 있는 별개의 공용 컨트롤이기 때문이다.
하지만 우리가 아주 특수한 사연이 있어서 리스트뷰 컨트롤 같은 거창한 물건을 직접 자체 구현이라도 하지 않는 한, 헤더만 끄집어내서 사용할 일은 별로 없을 것 같다.

지금까지 소개한 4종류의 모드를 정리하자면, 아이콘 모드들은 align을 어찌 하느냐에 따라서 상하와 좌우 스크롤바를 모두 볼 수 있고, '목록' 모드는 좌우 스크롤바만 볼 수 있다.
'자세히' 모드는 개수가 초과될 때는 상하 스크롤바이고, 아이템을 표시하는 폭이 초과됐을 때만 좌우 스크롤바를 볼 수 있다.

그리고 아이콘 모드는 기존 리스트박스에는 전혀 없던 새로운 기능이며, 기존 리스트박스와 가장 비슷한 모드는 '자세히' 내지 '목록' 모드라는 것을 알 수 있다. 이 두 모드에서는 아이콘은 필수가 아닌 그냥 선택, 옵션이다. 기존 리스트박스처럼 그림 없이 글자를 출력하는 용도로만 써도 된다.

이들에 비해 '작은 아이콘' 모드는 정체성이 불분명해서 사실 잘 쓰이지 않는다. 아이콘을 강조하고 싶으면 '큰 아이콘'으로 가면 되고, 좀 더 예쁘게 일목요연하게 아이템들을 출력하려면 '목록'(간단히) 또는 '자세히'로 가면 되기 때문이다. 저 그림에서도 보다시피 작은 아이콘은 폭이 들쭉날쭉이어서 보기에도 별로 좋지 않다.

그래서 Windows XP에서는 제5의 새로운 모드가 추가됐다. 바로..

(5) 타일
큰 아이콘을 사용하는데, 주 텍스트는 아이콘의 아래가 아닌 오른쪽에 출력된다.
아이콘이 좀 큰 편이니 주 텍스트의 아래에도 여유 공간이 생기는데, 거기에는 부 텍스트 중에서 사용자가 지정한 것을 덤으로 출력해 준다.

사용자 삽입 이미지

이것도 굉장히 참신한 발상인 것 같다. 타일의 폭은 사용자가 임의로 지정 가능하다.
align은 아이콘 모드처럼 left와 top을 모두 지정 가능하다. 다만, 아이템들의 위치까지 아이콘 모드처럼 임의 지정 가능한지는 잘 모르겠다.

원래 리스트뷰 컨트롤의 보기 모드는 4종류이다 보니.. 윈도우 스타일에서 0부터 3까지 딱 2개의 최하위 비트를 사용하여 지정하게 돼 있었다.
컨트롤을 생성하고 아이템들을 잔뜩 추가한 뒤에도 모드를 변경할 수 있었다. SetWindowLongPtr을 이용해서 스타일 값을 변경하면 컨트롤이 이를 인식해서 모드를 변경했다.

그런데 제5의 모드는 이런 식으로 지정할 수 없게 됐다. 리스트뷰 컨트롤은 기능이 워낙 너무 많아서 스타일, 확장 스타일, 거기에다 자신만의 고유한 전용 확장 스타일까지(LVM_SETEXTENDEDLISTVIEWSTYLE) 비트 플래그들이 꽉 찼기 때문이다.
결국은 LVM_SETVIEW라고 보기 모드를 지정하는 전용 메시지가 추가됐다. 새로운 보기 모드를 겨우 하나 추가하기 위해서였다.

네이버나 다음의 블로그들만 들어가 봐도 제목 목록만 표시, 본문까지 약간 포함해서 타일 형태로 표시.. 처럼 적어도 두세 종류의 보기 모드가 있는 걸 알 수 있다. 리스트뷰도 그런 식으로 그림과 글자의 표시 비율, 아이템당 전체 크기 같은 다양한 변수를 이런 식으로 제어할 수 있다고 생각하면 된다.
아이콘이 들어갈 자리에 사람 얼굴이 들어가면 무슨 인사기록표나 선거 후보 목록을 출력할 수 있을 것이고, 한자가 들어가면 옥편· 자전 내용을 이런 식으로 출력할 수 있을 것이다.

그럼 이제부터는 리스트뷰 컨트롤의 주요 개념이나 기능에 대해서 분야별로 간단히 소개한 뒤 글을 맺도록 하겠다.

1. image list

리스트와 트리 컨트롤은 아이템들 옆에 출력할 다양한 종류의 아이콘 그림들을 한데 관리하기 위해서 무슨 HICON을 몇백 개 내부적으로 관리..하는 건 아니고 image list라는 자료 구조를 공통으로 사용한다. image list는 마치 애니메이션 프레임처럼 크기가 동일한 여러 그림들의 배열이라고 생각하면 되며, 아이콘 핸들도 물론 손쉽게 등록할 수 있다. 투명색은 이미지 내부의 특정 배경색 또는 별도의 마스크 비트맵 중 편한 것으로 지정 가능하다.

또한 트리에서는 작은 아이콘이라는 한 종류만 사용하지만, 리스트 컨트롤에서는 구조적으로 큰 아이콘, 작은 아이콘 두 종류를 나눠서 지정 가능하다.
그리고 한 아이템의 아이콘에 대해서 여러 종류의 이미지를 한데 겹쳐서(overlay) 지정할 수도 있다. 파일이라면 '바로가기'임을 나타내는 자그마한 화살표라든가, 버전 관리 시스템에서 Up-to-date, modified 같은 상태를 나타내는 자그마한 modifier 그림이 바로 아이콘 overlay를 이용해서 표시된 것이다.

2. 그룹 분류

Windows XP에서는 타일 모드에 이어 리스트뷰 컨트롤에 아주 획기적인 기능이 하나 추가됐는데, 바로 '그룹' 기능이다. 필요하다면 그룹 내지 카테고리라는 것을 등록해 놓은 뒤, 아이템들별로 소속 그룹을 지정하면 이것들이 그룹별로 분류되어 딱 일목요연하게 표시된다.

사용자 삽입 이미지

그룹이 처음으로 도입된 XP에서는 이것 말고 다른 기능은 없다. Vista에서는 그룹에 대해 [+], [-] 버튼을 눌러서 마치 트리 컨트롤처럼 collapse/expand이 되게 하는 기능이 추가되었다. 단, 응용 프로그램에서 그게 가능하도록 별도의 비트 플래그를 넣은 그룹에 대해서만 그렇게 동작한다.
그룹은 다른 보기 모드에서는 다 지원되고 '목록' 모드만 열외이다.

3. 수많은 기능과 복잡한 API

리스트뷰 컨트롤은 당장 마소에서도 적극 사용하고 있다 보니, 자기 필요에 따라서 이것저것 수많은 기능들이 추가돼 왔다.
특히 IE4 시절에는 Active 데스크톱이니 뭐니 하면서 뭐든지 웹페이지처럼 보이게 하는 게 유행이었다. 리스트뷰 컨트롤의 아이템을 클릭하는 것조차 밑줄 쳐진 링크를 클릭하는 것과 비슷하게 보이게 하는 옵션은.. 음~ 정말 비장함이 느껴진다.

리스트뷰는 기능이 너무 많고, 공용 컨트롤 특유의 그 조작감까지 더해져서 다루기가 귀찮고 까다롭다. 리스트박스처럼 간단하게 LB_ADDSTRING + "문자열" 한 방으로 아이템을 추가할 수 없다. 뭘 더하고 고치려면 기본적으로 LVITEM 구조체 선언하고 마스크 플래그 지정하고..

더구나 문자열 부분 멤버는 읽기 쓰기 겸용으로 모두 쓰인다. Set 용도로 읽기 전용 문자열 포인터를 집어넣으려 해도 부득이하게 PTSTR 멤버에다가 const_cast를 해 줘야 된다. PTSTR과 PCTSTR을 공용체로라도 좀 같이 넣어 주지 하는 아쉬운 생각이 든다.

그리고 아이템 drag & drop은 컨트롤에서 우리에게 이벤트만 날려 주고 그걸로 끝이다. 드래그용 이미지를 생성하고 마우스 포인터 모양을 바꾸고 실제로 drop 처리를 하는 것, 아이콘 모드의 경우 실제 위치를 변경하는 LVM_SETITEMPOSITION 요청 따위는... 머리부터 발끝까지 사용자가 전부 일일이 구현해야 한다. 이거 일이 여간 번거로운 게 아니다.

헤더 클릭 정렬도 마찬가지다. 컨트롤이 자동으로 해 주지 않는다. 클릭된 헤더에 대해서 오름차순/내림차순/무정렬 상태를 나타내는 ▲▼ 모양을 표시하는 것, 다른 헤더에 있던 마크는 제거하는 것까지 전부 미주알고주알 우리가 해 줘야 되며, 아이템 비교 함수도 우리가 공급해 줘야 한다.
좋게 말하면 customize의 폭이 큰 것이고 나쁘게 말하면.. 귀찮다. 물론 재래식 리스트박스는 한번 등록된 아이템은 텍스트를 고치거나 순서를 변경하는 기능 자체가 전무했으니 그것보다는 상황이 나아진 셈이다.

4. 시스템 색상 변경

어떤 윈도우가 WM_PAINT를 받아서 자기 내용을 그릴 때, 매번 GetSysColor나 GetSysColorBrush를 호출하고, 매번 색깔을 새로 지정하고 펜과 브러시를 새로 생성한다면.. 시스템 색상이 나중에 달라지더라도 별 상관 없다.
하지만 성능을 위해서 이런 GDI 개체를 보관해 놓는다거나, 특정 시스템 색상이 합성된 상태로 비트맵 같은 걸 저장하고 있다면(일종의 캐싱).. 그것들은 시스템 색상이 바뀌었을 때 갱신되어야 한다.

이 상태를 알리는 메시지가 바로 WM_SYSCOLORCHANGE이다. 이제는 macOS조차도 최신 10.14 '모하비'에서 Dark 테마가 추가되었으니 시스템 색상 변경과 비슷한 개념이 도입된 셈이다. Windows는 다른 색깔 테마들은 다 없어졌지만 고대비 블랙/화이트만이 특수한 용도로 남아 있다.

WM_SYSCOLORCHANGE는 top-level 윈도우들에게 전파된다. 차일드에 속하는 리스트뷰 컨트롤이 이 메시지를 직접 받지는 못한다. 아이콘을 사용하지 않을 때는 별 문제가 없는데, 아이콘을 사용하는 컨트롤에 대해서 이 메시지를 수동으로 전해 줘야 한다. 그리하지 않으면 화면 배경의 흑백이 바뀌어도 쟤는 그게 반영되지 않아서 색깔 배색이 어색해지더라.

색깔 변경 통지도 마치 클립보드의 내용 변경 통지처럼 원하는 윈도우가 신청하면 top/bottom 위상을 불문하고 직통으로 받을 수 있어야 하지 않나 싶다. 이렇게 부모 윈도우가 일일이 전해 줘야 하는 건 디자인상 문제가 있어 보인다.

5. Ctrl+휠 인식

리스트뷰 컨트롤 내부에서 마우스 휠이 굴러갔다면 그렇다면 창 내부를 스크롤 하면 된다. 즉, 자체적으로 처리하면 되고 굳이 부모 윈도우에게 알려 줄 필요가 없다.
하지만 Ctrl+휠은 화면 확대 배율을 변경하는 용도로 쓰이는 게 요즘 추세이다. 응용 프로그램마다 자기가 사용하는 리스트뷰에서 지원하고자 하는 모드가 다를 테니, 이를 운영체제에서 임의로 일괄적으로 자동 지원할 수는 없다.

결국 Ctrl+휠은 그냥 휠과는 달리 부모 윈도우로 통지해 주는 게 바람직해 보인다. 하지만 이와 관련된 event notification은 공식적으로 존재하지 않는다. 탐색기는 Ctrl+휠을 어떻게 구현했는지가 궁금해진다. 하긴, 탐색기는 리스트뷰 컨트롤도 워낙 많이 마개조했으니 윈도우 프로시저를 서브클래싱 해서 메시지 전체를 통째로 가로채 버렸다면 그 정도 구현쯤은 일도 아니긴 했을 것이다.

6. 체크리스트 모드에서의 버그

리스트뷰 컨트롤(그리고 트리 컨트롤도)에는 모든 항목들에 대해 체크박스를 넣는 옵션이 있다.
보통은 아이콘 자리에다가 체크박스 이미지를 집어넣는 꼼수를 동원해서 야메로 체크리스트 모드를 구현하는 편인데.. 이 옵션은 이미지와 별개로 체크박스를 또 표시해 준다.

이 기능은 공용 컨트롤이 처음 개발되던 때부터 있었던 건 아니고 Windows 98 + IE4 내지 5 타이밍 때 추가되었다. 이 기능이 처음부터 지원됐다면 리스트뷰 컨트롤의 Selection model이라는 속성 하에서 "단일 선택 / 체크박스 / 복수 선택" 중의 한 옵션으로 지원되는 게 바람직했을 것이다. 체크박스 모드에서 또 복수 선택을 사용할 일은 없을 테니까 말이다.

그리고 체크리스트 모드는 그 정의상 보기 모드들 중에서는 '목록' 모드와 가장 잘 어울리고 아니면 기껏해야 '자세히' 모드와도 추가로 어울린다. 큰 아이콘이 부각되는 모드와는 아무래도 영 어울리지 않는데, 그래도 원한다면 그 모드에서도 체크리스트를 사용할 수는 있다.
다만, 이 모드에서 키보드나 마우스로 체크 표시를 반복하면 선택막대가 갈수록 진해지는데.. 이건 명백히 버그로 보인다. 고전 테마나 XP~7 같은 구버전에서는 이런 현상이 없었고 Windows 8~10에서만 저런다!

사용자 삽입 이미지


Posted by 사무엘

2019/01/13 08:36 2019/01/13 08:36
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1575

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

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

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

사용자 삽입 이미지

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

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

사용자 삽입 이미지

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

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

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

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

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

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

사용자 삽입 이미지

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

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

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

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

Posted by 사무엘

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

1. WM_CREATE의 리턴값/타입에 의문

Windows에서 C/C++로 GUI 프로그래밍을 할 때 WM_CREATE 메시지는 기본 필수 0순위로 접하게 되는 물건이다. 메시지 번호부터가 WM_NULL 다음으로 당당하게 1번이다.
얘가 오면 윈도우 프로시저는 lParam의 값으로 날아온 CREATESTRUCT 구조체 내용을 참조하면서 자신에 대해 초기화를 하고, 필요하다면 자기의 위치와 크기도 변경하고, 내 밑의 차일드 컨트롤들도 적절히 생성하면 된다.

그런데 이 메시지를 처리하고 나서 되돌리는 리턴값은 약간 이상한 형태이다. 성공하면 0, 실패하면 -1을 되돌리라고 명시되어 있다. 윈도우 프로시저가 실패값을 되돌리면 CreateWindow(Ex) 함수의 동작도 실패하여 창이 생성되지 않으며, NULL이 돌아온다.

즉, WM_CREATE의 리턴 형태는 BOOL이나 마찬가지이다. 그런데 왜, 어째서 직관적인 TRUE (1) / FALSE (0)가 아니라 이것보다 1 작은 값 형태로 정해진 걸까? (0 / -1)
이 때문에 MFC에서도 CWnd::OnCreate는 리턴 타입이 int로 설정되었다. 하지만 얘는 성공/실패만 따지기 때문에 원래는 int가 필요하지 않다. 내가 실험해 보니 굳이 0이 아니어도 -1을 제외한 다른 모든 값들은 성공이라고 간주되기는 하더라.

WM_CREATE는 대화상자 프로시저(DialogProc)처럼 평소에는 BOOL을 되돌리지만 몇몇 소수의 메시지에 대해서는 예외적으로 정보량이 더 많은 리턴값을 직접 되돌려야 하기 때문에 불가피하게 INT_PTR 형태로 설계된 것도 아니다. 더구나 WM_CREATE의 전신격인 WM_NCCREATE는 평범한 BOOL TRUE/FALSE 형태인 것도 의문을 더욱 증폭시킨다.

이와 관련해 혹시 숨겨진 사연이 있는지 레이먼드 챈 아저씨가 블로그에서 한 번쯤 다뤘을 법도 해 보이는데 내가 검색한 바로는 의외로 없다.
"CreateFileMapping은 실패값이 NULL인데 CreateFile은 실패값이 왜 혼자 INVALID_HANDLE_VALUE (-1)인가요?"와 거의 같은 맥락의 내력 의문점인데도 말이다.

파일 API의 경우, 먼 옛날에는(16비트 시절?) CreateFile이 지금 같은 형태의 핸들값이 아니라 파일 식별자 번호를 되돌렸으며, 0도 특수한 용도이지만 올바른 파일 식별자 값으로 예약돼 있었기 때문에 실패값을 -1로 따로 정한 거라고 설명이 돼 있다.

그렇다면 WM_CREATE도 처음에 설계하던 당시에는 굳이 BOOL로 국한되지 않고 0을 포함한 다양한 범위의 성공 리턴값을 되돌릴 수 있게 만들었는데.. 그럴 필요가 없어지면서 결국 지금 같은 형태로 굳어진 게 아닌가 싶다.

2. NC 버전과의 관계, 창의 소멸

Windows의 메시지 중에는 클라이언트 영역의 바깥 테두리를 그리거나(PAINT, ACTIVATE) 거기 크기를 정하거나(CALCSIZE) 그 영역의 마우스 동작을 감지하는(MOUSE*, ?BUTTON*) 용도로 WM_NC*로 시작하는 것들이 있다. 여기서 NC는 non-client를 의미한다.

그런데 WM_CREATE와 WM_DESTROY에도 WM_NC버전이 있다. 이때 NC는 딱히 외관상으로 클라이언트 바깥의 테두리나 제목 표시줄 같은 걸 가리키지는 않으며, 다른 방향으로 의미를 갖는다.
소멸 버전의 경우, WM_DESTROY는 아직 자기 밑에 자식 윈도우들이 멀쩡히 남아 있을 때 호출된다. 즉, 호출되는 순서가 top-to-bottom이다. 그러나 WM_NCDESTROY는 WM_DESTROY가 전달되었고 자식 윈도우들이 모두 소멸된 뒤에 자식에서 부모 순으로 bottom-to-top으로 호출된다.

즉, 어떤 윈도우가 윈도우 프로시저를 통해 가장 마지막으로 받는 메시지는 WM_DESTROY가 아니라 WM_NCDESTROY이다. WM_QUIT은 아예 스레드의 메시지 큐 차원에서 Get/PeekMessage를 통해 전달받을 뿐, 특정 윈도우의 프로시저로 오지는 않으니까...

어떤 윈도우 핸들과 C++ 객체가 연결되어 있는 경우, WM_NCDESTROY에서 그 객체를 delete 해 주면 된다. 그 전 단계인 WM_DESTROY에서 delete를 해 버리면 아직 소멸되지 않은 자기 자식 윈도우가 부모 윈도우의 C++ 객체 같은 걸 여전히 참조할 때 문제가 발생할 수 있다.

사용자가 Alt+F4를 누르거나 창의 [X] 버튼을 누르면 그 윈도우로 WM_CLOSE 메시지가 전달된다. 시스템 메뉴에서 닫기를 누른 것도(WM_SYSCOMMAND + SC_CLOSE)도 디폴트 처리는 WM_CLOSE 생성이다.
그리고 이 메시지에 대해 윈도우 프로시저가 다른 처리를 하지 않고 DefWindowProc으로 넘기면 그때서야 이 윈도우에 대해 DestroyWindow 함수가 호출되고 WM_DESTROY와 WM_NCDESTROY가 차례로 날아온다.

DestroyWindow는 호출하는 주체와 동일한 스레드가 생성한 윈도우들만 없앨 수 있다. 프로세스/스레드 소속이 다른 윈도우는 없앨 수 없으며, 그런 윈도우를 상대로는 WM_CLOSE를 보내서 창을 없애 달라는 간접적인 요청만 할 수 있다.

악성 코드 급의 프로그램이 아니라면 이 요청을 무작정 거부하는 끈질긴 윈도우는 없을 것이다. 하지만 일반적인 프로그램의 경우, "이름없음 문서를 저장하시겠습니까?"라고 질문을 해서 사용자가 취소를 누르면 자기가 종료되지 않게 하는 처리 정도는 한다. 작업 관리자의 '프로세스 종료' 기능은 응용 프로그램 창에다가 WM_CLOSE부터 먼저 보내 보고, 그래도 말을 안 들으면 TerminateProcess라는 극약 처방을 하는 식으로 동작한다.

그런데 WM_DESTROY 메시지를 받았는데 자기 자신에 대해서 DestroyWindow를 또 호출하는 이상한 프로그램도 있는가 보다. 창을 없애라는 요청은 WM_CLOSE이고, 이때는 그냥 DefWindowProc만 호출해도 알아서 소멸이 된다. WM_DESTROY는 요청이 아니라 이 창이 없어지는 건 이미 정해졌고 피할 수 없는 운명이라는 통지일 뿐인데.. 이때 DestroyWindow를 호출하면 같은 창에 대해서 WM_DESTROY가 이중으로, 재귀적으로 전달되는가 보더라.

소멸 중인 윈도우에 대해서 DestroyWindow 요청은 가볍게 무시만 해도 될 듯하지만 이미 이런 식으로 정해지고 정착해 버린 동작은 호환성 차원에서 함부로 고치지는 못한다고 한다.

3. 창의 생성

소멸 얘기가 좀 길어졌는데...
생성 버전에 속하는 WM_CREATE와 WM_NCCREATE 짝은 소멸 관련 메시지와 같은 유의미한 차이가 없긴 하다.
자식 컨트롤들을 생성하는 건 그냥 WM_CREATE 때 하면 되지, NCCREATE 때 딱히 해야 할 일은 없다. 쟤는 그냥 NCDESTROY와 짝을 맞추기 위해 도입된 것에 가깝다.

어떤 창이 생성되면, CreateWindow(Ex) 함수가 실행되어 있는 동안 WM_CREATE만 오는 게 아니라 WM_GETMINMAXINFO, WM_NCCREATE, WM_NCCALCSIZE가 먼저 전달된다. CREATE말고 나머지 메시지들은 창 내부의 공간 배분과 관계 있는 것인데, 아주 특수한 형태로 동작하는 유별난 윈도우가 아니라면 그냥 다 디폴트로 넘겨도 무방한 것들이다.

그리고 앞서 살펴본 바와 같이, CREATE 계열 메시지들은 실패값을 리턴함으로써 이 창의 생성을 저지할 수 있다.
WM_NCCREATE의 실행이 실패한다면(FALSE) 이 창은 그 뒤로 곧장 WM_NCDESTROY만 날아온 뒤 소멸되어 버린다. 그러나 WM_CREATE에서 실패하면(-1) WM_DESTROY와 WM_NCDESTROY가 차례로 날아오면서 소멸된다.

그런데 여기서 유의할 것이 있다.
프로그램의 main 윈도우의 경우, WM_DESTROY를 받았을 때 대체로 main message loop을 벗어나고 프로그램 전체를 종료하기 위해서 PostQuitMessage를 호출한다.
이게 일단 호출된 뒤부터는 이 스레드에서는 다른 GUI 윈도우를 생성한다거나 message loop을 돌아서는 안 된다. 여기에는 에러 메시지를 출력하기 위한 간단한 MessageBox 호출도 포함된다.

main 윈도우의 생성이 WM_CREATE 단계에서 실패했다면(WM_NCCREATE은 무관) WM_DESTROY를 거치게 되며, 특별한 조치가 없는 이상 그 메시지의 handler에 있는 PostQuitMessage도 처리되었을 것이다. 이 상태에서

if(::CreateWindowEx( .... )==NULL) {
    ::MessageBox(L"프로그램 실행 실패");
    return 1;
}

이런 식으로 코드를 쓰면 MessageBox 내부의 메시지 loop은 메시지 큐에서 WM_QUIT이 튀어나오기 때문에 곧바로 끝난다. 즉, 메시지 박스가 화면에 표시되지 않는다는 것이다.
그러니 에러 메시지를 찍을 거면 차라리 WM_CREATE 내부에서 -1를 리턴하기 전에 하는 게 낫다.

심지어 main 윈도우의 WM_NCDESTROY에서 MessageBox를 호출하려 시도하는 경우도 있다고 한다. 프로그램 실행이 다 끝난 마당에 무엇을 찍을 일이 있는지는 모르겠지만 이 역시 위와 동일한 이유로 인해 메시지 박스가 화면에 나타나지 않는다.
뭐, WM_DESTROY 대신 WM_NCDESTROY에서 PostQuitMessage를 요청할 수도 있겠지만 int main(int argc, char *argv[]) 대신에 char **argv만큼이나.. 익숙한 관행은 아니어 보인다.

이상. 이렇게 간단하고 익숙한 주제를 갖고도 지금까지 진지하게 생각하지 못한 것에 대해 할 말이 많을 때가 흥미롭다.

Posted by 사무엘

2018/12/12 08:31 2018/12/12 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1564

1. 3차원 그래픽 시연 프로그램의 개편

지난 2003년부터 2005년 사이, 본인의 대학 재학 시절에 만들어진 뒤 10년이 넘게 버전업이 없었던 '3차원 그래픽 시연 프로그램'이 정말 오랜만에 업데이트 되었다. 무엇이 바뀌었냐 하면.. '시선 고정' 모드란 게 추가됐다.

이 프로그램은 지금까지 3D FPS 게임처럼 앞뒤 좌우 상하로 움직이는 것에만 최적화돼 있었는데, 시선 고정 모드에서는 카메라가 어디 있든지 시선이 언제나 기준점을 향해 고정되게 된다.
시선이 고정되니, 이때는 통상적인 좌우 화살표나 page up/down을 이용한 시점 변경이 동작하지 않는다. 끄덕끄덕(pitch)/설레설레(yaw) 말고 시선에 영향을 주지 않는 갸우뚱(roll)만 동작한다.

그리고 좌우 게걸음인 ZX와 상승/하강인 QA를 누르면 그냥 움직이는 게 아니라 기준점과 같은 거리를 유지하면서.. 기준점 주변을 상하 좌우로 돌게 된다.
요것이 본인이 원하던 바였다. 사실, 3차원 그래픽 편집 프로그램에서는 FPS 게임 같은 움직임보다는 이렇게 시선이 고정된 '빙글빙글' 앵글 이동을 더 흔히 볼 수 있었을 것이다. 이 동작을 내 프로그램도 뒤늦게 지원하게 됐다.

F를 누르면 시선 고정 모드를 켜거나 끌 수 있다. 아래의 상태 표시줄에 기준점의 좌표가 같이 나타난다.
그리고 D를 누르면 지금 카메라가 있는 위치를 기준점으로 지정한다. 초창기에는 (0, 0, 0) 원점이 기준점이다.
기준점을 파란색 동그라미 같은 별도의 부호로 표시해 주면 사용자에게 도움이 될 수 있겠지만.. 일단은 그런 걸 생략했다.

예제 데이터들 중에서는 구(sphere)가 제일 볼 만할 것이다. 이 구는 그렇잖아도 원점이 중심으로 만들어져 있다. 구에서 적당히 떨어진 뒤 시선 고정 모드를 켜고 QA/ZX를 누르면 우리가 인공위성이라도 된 것처럼 구 주변을 빙글빙글 돌게 된다.
그에 비해 토러스(torus)는 원점을 기준으로 만들어져 있지 않은 듯하니(구체적인 값은 기억이..) 적당히 다른 점을 기준으로 설정해야 튜브 안을 이탈하지 않고 빙글빙글 돌 수 있다.

사용자 삽입 이미지

지난 2016년에는 삼각형 오심 그리는 프로그램을 오랜만에 업데이트 했는데.. 이번에는 3차원 그래픽 프로그램을 손보게 되니 감회가 새롭다. '옛날 자료실'에 있는 프로그램들도 이런 식으로 최소한의 유지 보수는 여전히 하는 중이다.

2. 날개셋 개발 관련 미스터리

본인은 하루는 키보드 보안 ActiveX를 사용하는 어느 사이트에서 날개셋 한글 입력기 외부 모듈이 뻗는 걸 발견했다. 한글만 연달아 입력하는 것은 문제가 없는데, 그렇게 조합을 만들었다가 숫자나 마침표 같은 기호를 찍어서 조합을 중단하면.. 에러가 나고 브라우저 창이 다시 열리곤 했다.

마소 IME 같은 타 프로그램에서는 괜찮고 내 프로그램에서만 100% 재연 가능한 문제가 뻔히 발견되었는데.. 그렇다면 이 문제는 독 안에 든 쥐나 마찬가지이고 곧바로 원인을 추적해서 해결되어야 할 것이다. 그런데 믿어지지 않지만 도저히 그러지를 못했다. 디버깅에 필요한 모든 절차와 방법론을 IE와 보안 유틸리티가 원천봉쇄하고 있었기 때문이다.

exception handler를 지정해서 뻗었을 때 덤프 파일을 만들려고 해도, 윗선에서 예외 이벤트를 가로채기라도 하는지 덤프가 만들어지지 않았다. (덤프는 프로그램이 뻗은 지점이 소스 코드상으로 어디이고 그 당시 함수들의 호출 계층이 어떠한지에 대한 정보를 담고 있음. 원인 추적에 매우 중요!)

입력란이 떠 있는 IE 프로세스에다가 디버거를 붙이면.. 보안 유틸이 이를 감지하고 디버거를 끄라고 요구하면서 동작을 거부했다.
뻗었을 때 디버거를 붙여도 문제의 프로세스는 상황을 확인할 틈도 없이 혼자 싹 종료되어 버렸다.

결국은 무식하게 키 입력이 감지됐을 때.. 등 의심되는 모든 곳에다가 화면/파일로 로그를 찍어서 테스트를 해 봤다.
그런데 이거 뭐 내가 짠 코드는 모조리 정상 통과한 뒤에 이상한 데 엄한 데서 에러가 발생하는 것이었다.

이건 정황상 키보드 보안 유틸과 3rd-party IME와의 충돌이긴 하지만 내가 아는 방법으로는 도저히 문제의 원인이나 해결책을 파악할 수 없어서 이번 9.61 버전에서도 부득이하게 해결되지 못했다. 언젠가 여유가 있으면 그 보안 유틸의 개발사와도 협조를 구해서 합동 수사 공조라도 해야 하지 않을까 싶다.

3. 스레드

어느 플랫폼에서든 프로그램을 짜다 보면, 백그라운드 스레드에서 뭔가를 열심히 수행한 뒤에 결과값을 표시하는 마무리 작업은 반드시 main UI 스레드에서 실행해야 할 때가 생긴다. 이에 대해서 본인은 예전에 글을 쓴 적이 있다.

요즘 프로그래밍 언어들은 언어 차원에서 별도의 블록을 분리해서 이 블록 안의 코드는 별도의 스레드에서 비동기적으로 실행되다던가, main UI 스레드에서 실행시키는 식으로 간편하게 제약을 가할 수 있다. 요런 걸 macOS의 Objective C에서도 보고 Java, C# 등에서도 봤던 것 같다.
그런 게 지원되지 않는 언어나 플랫폼에서는 해당 기능을 직접 구현하게 되는데.. Windows라면 메시지를 보내는 것과 일맥상통한다. main UI 스레드라면 그 정의상 message loop을 돌리고 있을 것이기 때문이다.

그런데 Windows용 IME는 자기가 만들지 않은 남의 프로그램 창, 남의 스레드, 남의 message loop을 기반으로 돌아가기 때문에 거기에다가 자신만의 메시지와 자신만의 메시지 핸들러를 슬쩍 얹기가 좀 난감하다.
그나마 옛날에 프로토콜이 IME 방식이던 시절에는 IME가 제각기 자신만의 보이지 않는 윈도우가 있었기 때문에 내부적으로 custom 메시지를 얼마든지 처리할 수 있었다. 하지만 TSF로 바뀐 뒤에는 그런 윈도우가 존재하지 않는다.

IME는 키보드 포커스를 받는 남의 윈도우에다가 WM_USER+* 이상한 메시지를 함부로 보내서는 안 되고, 타이머 ID도 함부로 선점하지 않아야 한다. 그러면 윈도우 핸들 없이 콜백 함수를 바로 호출하는 타이머밖에 선택의 여지가 없는데.. 그렇게 하면 SetTimer를 호출하는 자신과는 다른 스레드 문맥에서 처리되는 타이머를 생성할 수가 없다.

이게 생각보다 굉장히 난감한 문제이다. 결국은 타 스레드에서 main UI 스레드 문맥으로 특정 코드를 실행하기 위해 본인은 TSF 모듈도 임시로 나만의 message-only 윈도우를 main UI 스레드에서 생성하고, 이 윈도우가 특정 메시지를 받았을 때 그 코드가 실행되도록 프로그램을 작성했다. 메시지라는 게 알고 보면 스레드 간 교통 정리에 큰 기여를 하고 있는 셈이다.

사실 스레드, 특히 콘솔 프로그램이 아니라 main UI가 있는 프로그램에서 스레드를 쓰는 건 십중팔구 사용자 입장에서 프로그램의 반응성을 올려 주기 위해서 하는 게 대부분이다. 일시불로 프로그램이 잠시 멈추고 뜸을 들이는 게 싫으니 이자를 감수하고라도 찔끔찔끔 할부를 선택하는 것이다.

스레드 그 자체는 메모리를 더 잡아먹고 컨텍스트 스위칭 오버헤드 때문에 전반적으로 CPU가 할 일을 더 늘리고 성능을 떨어뜨린다. 하지만 스레드를 만들어서 컴퓨터가 더 많이 고생할수록 사용자의 입장에서는 더 편리한 기능이 많이 구현되는 것이 사실이다.

4. 날개셋 외부 모듈과 입력 패드의 동작 차이

날개셋 한글 입력기의 구현체 프로그램 중에서 편집기는 오로지 자기 혼자서만 돌아가는 프로그램이니 다른 고민의 여지가 없는데.. 외부 모듈과 입력 패드는 본격적으로 타 프로그램에다 문자를 보내기 때문에 다양한 환경에서 최대한 동일한 동작을 보장해야 한다. 그게 불가능한 경우 불가능한 한계에 대해서 사용자에게 미리 고지를 해야 한다.

Windows용 IME의 입장에서 좀 별종인 환경은 전통적으로 (1) 로그인(잠금) 화면, 그리고 (2) 명령 프롬프트가 있다. 그리고 Windows 8/10부터는 (3) Metro UI도 추가됐다.
입력 패드는 원래 명령 프롬프트에서는 전혀 동작하지 못하다가 Windows 7에서부터는 동작 가능해졌다.

외부 모듈은 Metro UI에서는 조합 중인 한글을 보내는 일반적인 동작은 가능하지만 tab, enter 같은 글쇠 입력을 보내지는 못한다(날개셋문자 또는 각종 입력 도구의 버튼을 통해서).
그 외에 Metor UI에서는 프로그램이 외부의 데이터 파일을 참조하지 못해서 입력 설정이 데스크톱 환경과 동기화되어 있지 않다거나, 문자표 같은 입력 도구들도 제대로 동작하지 않는 한계가 있다. 입력 도구를 X 버튼을 눌러서 닫을 수도 없다. (우클릭 메뉴를 이용해야..)

한편, 입력 패드는 외부 모듈과 달리 자신이 독립된 프로세스이기 때문에 파일을 못 여는 한계는 없다. 단지, 반대로 Metro UI로 조합 문자 같은 입력 동작을 보낼 수 없을 뿐이지..;;
그런데 굉장히 의외로 글쇠 입력을 보낼 수는 있다. 가령, 화면 키보드에서 ㄱ, ㅏ 같은 한글 조합 글쇠는 못 보내지만 tab, enter 버튼을 누른 것은 전해진다는 뜻이다. 외부 모듈이 할 수 없는 일을 입력 패드가 예외적으로 할 수 있으니 흥미로운 차이점이다.

외부 모듈에서 글쇠 입력을 못 보냈거나 입력 패드에서 자체 입력을 못 보냈을 때, 못 보냈다는 에러 메시지라도 출력할 수 있으면 좋겠지만 일단은 방법이 없어 보인다.

외부 모듈과 입력 패드에는 이것 말고도 흥미로운 차이점이 더 있다.
외부 모듈은 Windows 메시지를 직접 받지 못한다는 특성 때문에 alt가 섞인 단축글쇠를 전혀 인식할 수 없다. 원래 alt는 운영체제 차원에서의 단축키나 메뉴 구동용으로 쓰이지 IME 같은 데서 가로챌 여지가 없기도 하고 말이다.

그 반면, 비록 프로토콜을 제대로 지원하는 소수의 프로그램 한정이긴 하지만 그래도 편집기와 다를 바 없는 온전한 A급 동작을 보장 가능한 것도 외부 모듈이다. 입력 패드로는 완성된 한글을 낱자 단위로 지우거나 달라붙는 자유자재 동작까지 지원하지는 못한다. 서로 일장일단이 있는 셈이다.

Posted by 사무엘

2018/12/09 08:35 2018/12/09 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1563

컴퓨터 프로그램은 실행되는 과정에서 단순히 주메모리만 읽고 쓰는 게 아니라, 디스크처럼 기록이 영구적으로 남는 보조 기억 장치에다가도 정보를 기록한다.
여기에 기록되고 보관되는 것은 단순히 사용자가 직접 만들어 내서 Save라는 명령을 내려서 저장하는 문서 데이터만이 전부가 아니다. 사용자가 지정한 프로그램 자체의 각종 옵션· 설정값도 저장되어서 나중에 이 프로그램을 다시 시작할 때 그대로 보존되곤 한다. 세심한 프로그램이라면 직전에 프로그램 창을 띄워 놨던 크기와 위치 같은 시시콜콜한 정보도 몽땅 다 기억해 놓는다.

이런 '설정 정보'를 저장하기 위해 프로그램들은 예로부터 자신만의 cfg 내지 config.dat 같은 파일을 만들어 두곤 했다. 본인이 개발한 날개셋 한글 입력기도 imeconf.dat라는 파일을 운영체제의 사용자 계정별로 고정된 디렉터리에다 만들어 놓는다. (물론 날개셋 프로그램의 경우.. 입력 설정이라는 게 단순한 프로그램 setting 수준을 넘어 그 자체가 이미 사용자가 새로 창조하는 '문서 데이터'라는 성격도 지닌다만...)

Windows에서는 이런 설정 파일을 저장하고 불러오는 절차를 정형화하기 위해서 ini(초기화 정보)라는 텍스트 파일 포맷을 도입했으며, 이걸 읽고 쓰는 [Get/Write][Private]Profile[Int/String]이라는 API 함수를 제공해 왔다. [섹션] 구분과 함께 "이름=값" 이런 게 주욱 들어가 있는 그 파일 말이다.

이들 함수는 파일을 읽고 씀에도 불구하고 뭔가 열어서 핸들값을 얻었다가 나중에 닫는 절차가 없는 게 특징이었다. 구현하는 사람 입장에서는 그리 좋은 설계 형태가 아닐 텐데..
Private이 안 붙은 API를 사용하면 자기 ini 말고 심지어 운영체제의 설정 파일인 win.ini에다가도 정보를 기록할 수 있었다.

ini 파일은 당장 사용하기는 간편하지만 한계와 문제점이 많았다. 가장 먼저 XML 같은 다단계 계층 구조를 지원하지 않았다(과거 Windows 3.x의 프로그램 관리자의 그룹이 계층 구조가 아니었던 것처럼).
그리고 아까 언급했듯이 핸들/상태 정보가 없는 API의 설계 형태에다가 텍스트 포맷이라는 점까지 겹쳐서 데이터가 방대해질 때의 처리 성능도 썩 좋지 않았다.

응용 프로그램이 있는 디렉터리, 또는 Windows 디렉터리에 온갖 자잘한 ini 파일들이 쌓이면서 지저분해지는 점 역시 문제였다. 그리고 저장을 할 거면 사용자 설정 데이터들 전용 디렉터리라도 마련해 놔야지, 그게 프로그램 실행 파일들이 있는 곳에 버젓이 저장되는 건 오늘날의 보안 내지 권한 관점에서 봤을 때 좋지 못한 설계 형태였다.

그래서 성능, 보안, 효율 등등을 모두 잡기 위해서 마소에서는 운영체제와 응용 프로그램들의 설정 저장은 파일 형태로 노출시킬 게 아니라 이를 전담하는 별도의 거대한 중앙집권 데이터베이스를 만들 생각을 하게 됐다. 그래서 이를 레지스트리라는 이름으로 일찍부터 도입했다.
마치 디렉터리 경로처럼 역슬래시로 구분된 계층 구조의 key를 만들고, 그 아래에 파일처럼 여러 개의 value들을 집어넣을 수 있게 했다.

물론 레지스트리는 구조적으로 장점도 있고 단점도 있다. 컴덕이나 프로그래머들 사이에서 호불호가 갈리며, 레지스트리를 아주 싫어하는 사람도 있다. macOS나 리눅스는 레지스트리 그딴 거 없이도 잘 돌아가고 더 안정적이기까지 하다고 주장한다.
하지만 레지스트리가 없다고 해서 그런 OS에 레지스트리가 하는 일 자체가 존재하지 않는 건 아니다. 그 OS에서도 운영체제나 응용 프로그램들이 자기 설정을 저장하는 공간이 있어야 할 텐데 도대체 어디에 저장되는 걸까?

프로그램을 제거한 뒤에도 그 프로그램이 써 놓은 레지스트리 데이터가 같이 지워지지 않아서 레지스트리가 갈수록 지저분해지고 통제불능이 된다는 식의 비판이 있다. 그런데 그건 ini/cfg 파일을 쌩으로 다룰 때도 부주의하면 어차피 똑같이 발생할 수 있는 문제이다. 한쪽에서 존재하는 문제는 대부분 다른쪽에서도 형태만 바뀐 채로 고스란히 존재할 수밖에 없다. 본인은 이런 논리를 근거로 레지스트리 무용론 급의 회의적인 시각에는 동의하지 않는다.

프로그램마다 설정 데이터라는 게 수십~수천 바이트, 정말 커 봤자 수만 바이트 남짓할 아주 작은 분량에 지나지 않는다. 그런 자잘한 데이터를 한데 관리해 주는 DB가 운영체제 차원에서 제공되면 편하면 편하지 상황이 더 나쁘지는 않을 것이다.

레지스트리는 딱 32비트 Windows 95/NT와 함께 등장했다. 이때부터 마소에서는 소프트웨어 개발자들로 하여금 구닥다리 INI 파일 함수를 쓰지 말고 ADVAPI32.DLL에 있는 Reg** 레지스트리 조작 함수를 사용할 것을 적극 권해 왔다.
다만, 이들 함수는 구닥다리 함수보다 받아들이는 인자가 많고 사용하기가 번거롭고 귀찮긴 하다. 마치 fopen과 CreateFile의 차이만큼이나 말이다.

그래서 별도의 클래스를 만들어서 쓰면 편하다. HKEY를 멤버로 가지면서 소멸자에서는 RegCloseKey를 해 주고, 각종 타입별로 인자를 다양하게 오버로딩한 Get/Set 함수를 만들고 말이다. 레지스트리 관련 API는 MFC에서도 의외로 클래스화를 전혀 하지 않았으니 이거 연구는 프로그래머들 개인 재량이다. MFC는 16비트 시절부터 있던 CWinApp 클래스의 [Get/Write]Profile* 함수를 상황에 따라 ini 대신 레지스트리 기록으로 대신하도록 동작을 확장했을 뿐이다.

사실, 16비트 Windows 시절에는 지금처럼 HKEY_* 어쩌구 하는 그런 형태의 레지스트리는 없었지만, 그 전신 비스무리한 건 있었다. Windows 3.1에서 그 이름도 유명한 OLE라는 기능 내지 개념이 추가됐기 때문이다.
응용 프로그램들의 설정 같은 건 몰라도 확장자별 연결 프로그램이라든가 OLE 서버 같은 정보들은 운영체제 차원에서 관리하는 별도의 바이너리 데이터 파일에 등재되었다(Windows\reg.dat). 그리고 여기에 정보를 사용하는 Reg[Open/Create/Close]Key와 Reg[Query/Set]Value 같은 함수가 있었다.

16비트 시절부터 이런 초보적인 함수가 있었는데 이를 확장하여 오늘날의 레지스트리가 된 것이다. 그렇기 때문에 오늘날 사용되는 레지스트리 함수들은 대부분 뒤에 Ex가 추가돼 있다. 옛날에는 루트 키로 HKEY_CLASSES_ROOT 하나만이 존재했었다.
이 점을 생각하면 옛날에는 지금 같은 레지스트리가 있지도 않았는데 지금 왜 RegCreateKeyEx, RegQueryValueEx 등등 Ex 함수를 써야 하는지 이유를 알 수 있다.

지금까지 Windows의 레지스트리 개념과 관련 API를 살펴봤으니, 다음으로는 역대 버전별 레지스트리 편집기에 대해서 살펴보도록 하겠다.
레지스트리 편집기는 과거 도스 시절로 치면 디스크 내부 구조를 저수준에서 수정할 수 있는 노턴 유틸리티의 DiskEdit와도 비슷한 아주 저수준 유틸리티이다. 아예 없어서는 안 되지만 일상적으로 쓸 일은 없으며(없어야 하며), 초보자에 의한 섣부른 조작은 절대 권장되지 않는 위험한 프로그램이다. 이걸 조작함으로써 운영체제를 맛이 가게 만들고 컴퓨터 부팅조차 안 되게 만들 수도 있기 때문이다. 그러니 보조 프로그램 그룹에 정식으로 등재될 일도 없다.

우리가 흔히 알고 있는 레지스트리 편집기는 Windows 95 시절부터 지금까지 큰 변경 없이 이어져 오고 있는 탐색기(왼쪽에 트리, 오른쪽에 리스트 컨트롤) 비슷한 형태의 프로그램이다.

사용자 삽입 이미지

(1) Windows 9x에서는.. 레지스트리는 내부적으로 Windows\System.dat와 Windows\User.dat라는 두 파일에 저장되었다. 그리고 HKEY_DYN_DATA라는 predefined root key가 있어서 여기를 들여다보면 일부 시시각각 변하는 시스템 정보 같은 걸 얻을 수 있었다고 한다.

그리고 Windows 9x용 레지스트리 편집기는.. 단순히 This program cannot be run in DOS mode가 아니라 유효한 도스용 코드가 stub으로 같이 들어간, 다시 말해 DOS/Windows 겸용 프로그램이었다.
레지스트리에 이상이 생겨서 Windows 부팅이 안 되더라도 레지스트리의 백업과 복구 정도는 도스에서 할 수 있게 하기 위해서이다. regedit.exe를 순수 도스에서 실행하면 놀랍게도 이런 옵션 안내를 볼 수 있다.

사용자 삽입 이미지

마소에서 프로그램을 이런 식으로 특수하게 빌드한 예는 극히 드물다. 이는 레지스트리 편집기가 그만치 특수한 프로그램임을 의미한다.

(2) 그럼 Windows NT로 넘어가기 전에, 더 옛날 Windows 3.x를 잠시 살펴보도록 하겠다.
이때도 레지스트리의 전신이 있었고 비스무리한 API도 있었듯이, regedit.exe라는 프로그램 자체는 있었다.
하지만 얘 역시 그룹에 정식으로 등록되어 있지는 않았으며, 이때 registry란 보다시피 그냥 확장자 별 연결 프로그램과 관련 DDE 명령.. 이런 것이 전부였다. 아까 언급했던 Windows\reg.dat의 내용을 편집한다.

사용자 삽입 이미지

저 때는 Properties도 '등록정보'라고 번역하고 Registry도 '등록 정보'라고 번역했다니.. 참 므흣하다. 띄어쓰기 하나 차이밖에 없다.
Windows 95부터는 확장자 연결 설정은 그냥 탐색기의 옵션에서 별도의 탭으로 들어가 있다. 그리고 98을 넘어서 2000/XP즈음부터 Property의 한국 마소 공식 번역은 '속성'으로 완전히 바뀌어서 지금에 이르고 있다.
그리고 이 시절의 regedit.exe에는 95의 것과 같은 유의미한 도스 stub이 들어가 있지도 않았다.

(3) 이제 오늘날 사용되고 있는 NT 계열 차례이다.
일단, 레지스트리가 저장되는 파일은 Windows\system32\config에 있는 default, software, system, components 같은 확장자 없는 파일들이다. 크기가 다들 수십 MB씩 한다.
그리고 각 사용자별 정보는 사용자 계정의 루트에 있는 ntuser.dat 파일이다.
9x와 NT 계열이 파일 포맷이 동일한지는 모르겠다. 지금의 NT 계열은 도스 부팅 기능이 없고, 운영체제가 가동 중일 때는 이들 파일들이 언제나 열리고 잠겨 있어서 일반 프로그램이 레지스트리를 파일 차원에서 들여다볼 수가 없다.

Windows NT 계열은 HKEY_PERFORMANCE_DATA라는 전용 root key가 있다. HKEY_DYN_DATA처럼 실제 레지스트리 데이터는 아니지만 레지스트리 API를 통해 시스템 정보를 얻어 오는 용도인데.. 이것도 비슷한 기능이 9x와 NT 계열의 구현이 서로 파편화된 경우가 아닌가 싶다. 로드된 프로세스/DLL 정보를 얻는 API만 해도 과거에는 두 계열이 서로 달랐으니 말이다.

그리고 Windows NT는 초창기 3.1 시절부터 지금과 같은 형태의 레지스트리를 갖고 있었기 때문에 자체적인 레지스트리 편집기도 보유하고 있었다. 바로 regedt32.exe이다.

사용자 삽입 이미지

얘는 만들어진 시기가 시기이다 보니, 탐색기가 아니라 구닥다리 파일 관리자 스타일로 만들어져 있다. 왼쪽의 계층 트리와 오른쪽의 값 목록은 모두 재래식 리스트 컨트롤 기반이다. 또한, 루트별로 제각각 다른 레지스트리 창들이 MDI 형태로 구성되어 있다.
오늘날 쓰이는 공용 컨트롤 기반 regedit.exe는 바로 regedt32.exe를 베껴서 새로 만들어진 거나 마찬가지이다.

Windows 2000, 그리고 확인은 안 해 봤지만 NT4에는 regedit와 regedt32가 같이 들어있었다. 기능이 대등하긴 하지만 regedit는 오리지널 NT용 유틸리티에 있던 '레지스트리 하이브'를 통째로 불러들이는 기능이 없었다.
그러다가 regedit에 이 기능이 들어가서 regedt32를 완전히 대체하게 된 것은 Windows XP부터이다. NT의 regedt32를 보면 메뉴가 뭔가 많이 있는 것 같은데, 실제로 열어 보면 별 거 아니다.

Windows NT의 레지스트리 편집기는 9x의 것과 달리 값의 이름과 데이터뿐만 아니라 REG_SZ, REG_DWORD 같은 타입도 표시해 줬다. 9x에서는 어려운 전문 용어라고 일부러 뺐던 것 같다.
그리고 이런 새 GUI 기반의 레지스트리 편집기는 오랫동안 REG_MULTI_SZ라고 0으로 구분된 복수 문자열을 편집하는 기능이 없어서 그냥 바이너리 에디터 창이 떴었다. 그러다가 regedt32와 기능이 통합된 Windows XP에서부터 한 줄에 하나씩 복수 문자열을 편집하는 기능이 도입되었다.

이렇듯, regedit와 관련하여 Windows 3.1, NT 3, 9x 등.. 복잡한 사연이 많은 걸 알 수 있다.
이 프로그램에는 레지스트리의 일부 구간을 별도의 파일로 저장하거나 도로 불러오는 기능이 응당 들어있는데, reg 파일은 아이러니하게도 key 이름이 []로 둘러싸이고 값들이 a=b 이런 식으로 쓰인 게 마치 과거의 ini와 형태가 비슷하다.

한번 만들어진 뒤에 딱히 기능이 바뀔 일이 별로 없는 프로그램이다만.. Windows 2000부터는 reg 파일의 인코딩이 UTF-16으로 바뀌었다. 그리고 Windows 10 어느 업데이트부터는 현재의 레지스트리 주소(key 이름)이 아래의 상태 표시줄 대신에 위의 주소 표시줄에 표시되고, 사용자가 거기에 인터넷 주소 치듯이 수동으로 입력을 해서 원하는 key로 바로 찾아갈 수도 있게 UI가 편리해졌다. 나름 굉장히 바람직한 개편이다.

※ 부록: 레지스트리 편집기에 준하는 위상의 자매품

1. sysedit

과거 Windows 3.x 시절에는 sysedit라는 이름으로 config.sys, autoexec.bat, win.ini, system.ini라는 4대 시스템 설정 파일만 한데 모아서 편집할 수 있는 MDI 텍스트 에디터가 있었다. 이게 Windows 9x는 말할 것도 없고 NT 계열 제품에도 32비트 한정으로 포함돼 있었다.
Windows에 내장돼 있는 극소수의 16비트 프로그램이기 때문에 날개셋 한글 입력기를 테스트 할 때도 개인적으로 유용하게 사용했다.

사용자 삽입 이미지

물론 Windows 9x는 config.sys와 autoexec.bat를 거의 퇴출시켰고, NT 계열은 뒤이어 두 ini 파일까지 완전히 퇴출시킨 거나 마찬가지다.

2. msconfig

Windows 98부터 잘 알려지지 않았지만 꽤 유용한 유틸리티가 추가됐다.
얘는 9x용은 sysedit가 하는 일을 모두 포함하면서(해당 파일에 내용을 추가하거나 기존 내용을 간편하게 제거), 레지스트리에 등록돼 있는 시작 프로그램들의 실행 여부를 제어하고, 각종 시스템 관리 유틸리티도 실행하는 기능을 제공했다.

사용자 삽입 이미지

이 프로그램은 레지스트리 편집기와 마찬가지로 오늘날까지도 남아 있기 때문에 명령 프롬프트에서 실행 가능하다.

Posted by 사무엘

2018/11/22 08:33 2018/11/22 08:33
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1557

Windows에서 돌아가는 GUI 프로그램은 커다란 자기 창을 띄우며, 그러면 작업 표시줄(task bar)에서도 그 창이 있다는 걸 표시해 준다.
그런데 작업 표시줄은 어떤 프로세스가 생성하는 여러 창들을 어떤 기준으로 선별하여 표시하는 걸까? 화면에만 표시되고 작업 표시줄에는 나타나지 않는 일명 '스텔스 윈도우'는 어떻게 만드는 걸까?

심지어 이 작업 표시줄에 등재된 창 목록은 Alt+tab을 눌렀을 때 나타나는 task 목록과도 완전히 일대일 대응하지는 않는 것 같다. 그 관계는 어떻게 될까?

일단, 작업 표시줄은 (1) child가 아니고(overlapped 또는 popup) owner(parent)가 NULL인 모든 윈도우를 자기 목록에 등재해서 표시해 준다. 대화상자건, 응용 프로그램이 클래스를 따로 등록한 윈도우건 모두 상관없다. 이건 대부분의 상황에서 충분히 합리적인 조치이다.

옛날에 대략 Windows XP 정도까지 쓰던 기억을 떠올려 보면, 디스플레이/키보드/마우스 같은 제어판 애플릿들은 분명 rundll32라는 독립된 프로세스로부터 구동된 대화상자임에도 불구하고 작업 표시줄에는 제목이 뜨지 않았다. 그 이유는 제어판 애플릿은 제어판 셸로부터 주어진 보이지 않는 부모 윈도우를 owner로 삼고 생성되기 때문이다.

그러므로 보이지 않는 윈도우를 생성하여 owner로 잡으면 대화상자뿐만 아니라 크기 조절 가능한 멀쩡한 overlapped 윈도우라도 작업 표시줄에 표시되지 않는 '스텔스' 형태로 만들 수 있다. 일반적으로 그런 건 만들 필요가 없고 그게 UI 디자인 상으로 권장되는 짓도 아니니 안 할 뿐이다.
그리고 저런 모델은 응용 프로그램의 입장에서는 자기 창이 하나가 아니라 두 개가 존재하는 셈이므로 창을 관리하는 게 약간 더 귀찮아진다.

한편, 위의 (1) 다음으로 몇 가지 단서가 있다. (2) WS_EX_TOOLWINDOW는 owner가 NULL이더라도 이 창이 무조건 작업 표시줄에 등록되지 않게 하고 심지어 Alt+tab 작업 목록에도 나타나지 않게 한다.
얘는 덤으로 제목 표시줄도 더 얇게 찍히게 한다(WS_CAPTION이 명시된 경우). 그래픽 에디터의 도구 팔레트처럼 작고 키보드 포커스도 안 받고, 화면에 언제나 표시되어 있는 그런 창을 찍으라고 있는 스타일인 것이다.

이 스타일 한 방이면 스텔스 윈도우를 아주 쉽게 만들 수 있다. 단지 제목 표시줄이 꼭 필요하고 그걸 다른 평범한 윈도우처럼 두껍게 만들고 싶을 때에만 owner 편법을 쓰면 될 듯하다.

그리고 다음으로.. WS_EX_TOOLWINDOW와 상극인 WS_EX_APPWINDOW 스타일이 있다. (3) 얘는 자기가 owner가 지정되었다 하더라도 반드시 작업 표시줄에 표시되게 한다.
Windows Vista인가 7부터는 디자인이 바뀌었는지 제어판 애플릿이 작업 표시줄에 나타나는 걸 볼 수 있다. 얘들은 여전히 owner 윈도우가 따로 있음에도 불구하고 저 스타일이 지정되었기 때문에 작업 표시줄에도 보인다. 중간에 뭔가 디자인 정책이 바뀐 듯하다.

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

내가 표시하는 대화상자나 창이 작업 표시줄을 건드리지 않고 조용히 떴다가 없어질지, 아니면 독립된 응용 프로그램처럼 뜰지 결정하는 것은 개발자의 재량이다. 다만, 작업 표시줄에다가도 나타나게 할 거면 창에다가 적절한 아이콘도 넣어 주는 게 좋을 것이다.

하지만 어지간해서는 owner가 NULL인 것만으로도 작업 표시줄에 창이 자동으로 등록되니 이 스타일이 굳이 따로 쓰일 일은 내 경험상 별로 없다.
WS_OVERLAPPEDWINDOW나 WS_POPUPWINDOW는 여러 기존 스타일들의 조합이지만 WS_EX_*WINDOW는 그렇지 않고 자신만의 고유한 값이다.

그리고 마지막으로 하나 살펴볼 게 있다.

MS Office Excel의 경우, 워크시트 문서창이 응용 프로그램 창의 내부에 소속된 child이다. 단독의 popup 윈도우 같은 게 아니다.
그럼에도 불구하고 한 프로그램에서 여러 파일을 열면 그 문서창들이 작업 표시줄에 제각각 나타난다. 이게 먼 옛날 Office 2000쯤부터 그렇게 되기 시작했는데.. 어떻게 이런 일이 가능한지 신기하지 않으신가?

이건 윈도우 스타일 조작만으로 가능한 동작이 아니고 별도의 API를 사용해서 구현한다.
셸 API들, 특히 작업 표시줄 근처에 있는 트레이(notification) 아이콘을 조작하는 API는 다 SH_*로 시작하는 고전적인 C 함수인 반면, 작업 표시줄을 조작하는 API는 ITaskbarList라는 COM 인터페이스 형태이다.

ITaskbarList를 얻어 온 뒤 HrInit를 호출해서 초기화하고, MDI 문서 창이 생성되면 자기 자신에 대해 AddTab + ActivateTab을 호출한다(ActiveTab도 반드시 해 줘야 됐음). 그리고 문서 창이 닫힐 때는 DeleteTab를 하면 된다. 이렇게만 하면 당장 날개셋 편집기조차도 얼추 Excel처럼 문서 창을 작업 표시줄에서 곧장 접근 가능하게 수정할 수 있다.

사용자 삽입 이미지

다만, 일이 마냥 간단하지만은 않다. 문서 창뿐만 아니라 기존 날개셋 편집기 자체의 창이 등록된 것도 같이 관리해야 하기 때문이다.
문서 창이 없거나 하나밖에 없으면 그냥 프로그램 자체의 창 하나만 유지하고 있고, 문서 창이 2개 이상이 되면 그때부터 프로그램 창은 날리고 문서 창들이 작업 표시줄에 나타나게 해야 한다. 불가능한 일은 아니지만 자동화가 돼 있지 않아 귀찮다. 마치 클립보드 viewer chain을 관리하는 것처럼 말이다.

요컨대, owner 윈도우, WS_EX_TOOL/APPWINDOW 스타일, 그리고 ITaskBarList 인터페이스만 기억하고 있으면 창과 작업 표시줄의 연계는 다 마스터했다고 볼 수 있겠다.
참고로 ITaskBarList는 초창기에는 이렇게 탭을 수동으로 등록하고 삭제하는 원시적인 기능만 있었다. 아마 IE 4나 5 시기쯤, 웹 브라우저와 운영체제 셸이 얽혀서 마개조되고 DLL hell 현상이 악명을 떨치던 20여년 전에 첫 등장했다.

그러다가 얘가 온갖 기능이 추가되어 ITaskBarList3으로 발전한 게 2000년대 말의 Windows 7 타이밍이다. 작업 표시줄에다가 응용 프로그램의 고유한 썸네일을 표시하고, 백그라운드 작업 진행률을 나타내고, 재생기의 경우 간단한 재생/멈춤 같은 버튼까지 갖다박는 UI 요소가 그때 추가됐기 때문이다. 그런 기능들을 바로 저 API를 통해 사용할 수 있다.

Posted by 사무엘

2018/10/17 08:34 2018/10/17 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1544

1. 실행 파일의 자가 업데이트

내가 개인적으로 만들어서 배포하는 날개셋 프로그램들에는 서버 트래픽, IME의 갱신과 관련된 Windows 특유의 기술적 난항, 개발자의 귀차니즘(..) 등의 이유로 인해 자동 업데이트 기능이 없다. 하지만 회사에서 상업용으로 개발하는 프로그램에다가는 프로그램의 자가갱신 기능을 구현할 일이 좀 있었다. 이를 위해서 Windows에서 자기 자신을 삭제하는 실행 파일을 만들 때와 비슷한 방식의 시나리오를 설정했다.

  1. 업데이트 대상인 app.exe는 새 버전인 app_new.exe를 다운로드하여 임시 디렉터리에다 보관한다.
  2. app.exe는 별도의 updater를 실행하고 자기 프로세스 ID와 임시 파일 이름을 명령 인자로 전한다. 그 뒤 자신은 신속히 종료한다.
  3. updater는 app.exe가 종료할 때까지 기다린 뒤, 실제로 종료되면 app.exe를 삭제한다. 그리고 임시 파일을 app.exe로 옮기고 개명한다.
  4. 그 뒤 updater는 새로 받은 app.exe를 실행한다.

그렇게 본 프로그램과 업데이터까지 잘 만들었다. 업데이터는 Program Files 아래의 디렉터리에서 파일을 지우고 생성할 것이기 때문에 관리자 권한으로만 실행되게 매니페스트 설정도 물론 해 놨다.

그런데 이렇게 만들어진 프로그램은 개발 환경을 포함해 여러 PC에서 업데이트 기능이 동작했지만, 일부 PC나 가상 머신에서는 제대로 동작하지 않았다.
관리자 권한을 분명히 줬는데 도대체 어디서 문제가 발생하는지, 그리고 같은 Windows 10에서 컴퓨터마다 성공 여부에 왜 차이가 발생하는지 기괴하기 그지없었다. 디버깅 결과 다음과 같은 복병을 발견하게 되었다.

(1) GetModuleFileNameEx는 만능이 아니다. 경로 정보를 얻고자 하는 모듈이 존재하는 프로세스의 비트수(32 또는 64비트)와.. 정보를 요청하는 나 자신의 비트수가 일치해야만 경로를 얻을 수 있더라.
난 이 함수가 그런 이유로 인해 실패할 수 있다고는 꿈에도 생각하지 않고 있었다. 문서에 딱히 언급이 없었으니까... 비트수가 안 맞으면 ERROR_PARTIAL_COPY라는 굉장히 낯설고 기괴한 에러 코드와 함께 함수의 실행이 실패했다. (Only part of a ReadProcessMemory or WriteProcessMemory request was completed.)

(2) 그리고 더 뒷목 잡는 상황은 따로 있었다.
업데이터는 app이 종료될 때까지 기다렸다가 파일을 대체하도록 만들어졌다. 그런데 컴퓨터에 따라서는 그 반대편으로 극단적인 상황도 발생했다. 바로, app이 너무 일찍 종료되거나 업데이터가 너무 늦게 실행되는 바람에 정작 OpenProcess 요청부터가 실패하여 app에 대한 정보를 전혀 얻지 못한 것이다.

그러니 이때는 app과 업데이터가 서로 공유하는 이벤트 같은 오브젝트라도 만들어서 app과 업데이터가 공존하는 타이밍이 반드시 존재하게 할 수도 있고, 아니면 app이 자신의 족적을 공유 메모리나 아예 임시 파일 같은 다른 방법으로 넘겨서 업데이터에다가 전달하게 문제를 회피해야 했다. 뭐, 이런 일이 있었다.

요즘은 어지간히 대단하고 전문적인 제품이 아닌 한, 소프트웨어를 받아서 사용하는 것 자체만으로 최종 사용자에게 금전적인 대가를 요구하는 관행은 사실상 없어졌다. 소프트웨어는 그냥 고객을 확보하고 개발사의 인지도를 올려서 더 교묘한 수단으로 수익을 추구하는 통로일 뿐이다.

소프트웨어를 무료로 뿌리는 대신에 개발사는 사용자로부터 그 소프트웨어의 사용 형태를 최대한 미주알고주알 수집하려 하는 게 요즘 추세이다. 단순히 (1) 최신 버전 체크/업데이트라든가 프로그램이 뻗어서 (2) 메모리 덤프 같은 디버깅 정보를 제출하는 용도로 서버와 교신하는 게 아니라, 자주 사용하는 기능 같은 전반적인 행동 데이터를 종종 개발사로 보낸다는 것이다.
물론 이건 사용자에 따라서 불쾌한 사생활 침해로 여겨질 수 있으니 사용자가 동의했을 때에만 보내게 돼 있다. MS Office, Visual Studio, Android Studio 등의 제품에 다 이런 옵션이 존재한다.

2. 경과 시간을 되돌리는 함수

자동 업데이트 기능은 마지막으로 업데이트 체크를 했던 시각을 레지스트리에다 저장해 둔다. 그리고 프로그램이 처음 실행되면 현재 시각이 레지스트리에 기록된 시각으로부터 1주 이상 경과했는지 검사한다. 조건이 만족되면 서버에 등록된 최신 버전을 지금 내 버전과 비교하고.. 이하 생략의 순서대로 구현되었다.

그런데 프로그램을 다 만들고 테스트를 해 보니, 아무리 서버에 더 높은 버전 정보가 등록됐고 그로부터 1주가 넘는 시간이 경과해도 이 프로그램은 자동 업데이트가 행해지지 않았다.
도대체 뭐가 문제인지 코드를 한참을 들여다 본 뒤에야 "아차...!" 하고 탄식을 내뱉었다.

GetTickCount()는 1970년 1월 1일 같은 고정된 epoch으로부터 경과한 시간을 되돌리는 게 아니라, 그냥 시스템이 부팅이 끝난 이래로 경과한 시간을 되돌리는 함수이지 않던가.
그러니 한번 컴퓨터를 켜 놓고 1주일 이상 버티지 않는 이상, 업데이트 체크가 행해질 리가 없었던 것이다.
내가 뭘 잘못 먹고 착각을 했는지, 코딩 하다가 졸았는지, 왜 그 상황에서 저 함수를 쓸 생각을 했는지는 알 길이 없다.

3. message loop

Windows에서 MFC를 안 쓰고 응용 프로그램 GUI를 구현하다 보면 결국 (a. PeekMessage) → GetMessage → (b. TranslateMDISysAccel, TranslateAccelerator) → (c. IsDialogMessage) → TranslateMessage → DispatchMessage 등의 순으로 함수를 호출하는 message loop도 손수 만들게 된다.
괄호를 친 a~c는 필수는 아닌 함수들이다. a는 idle time processing이 필요할 때만 쓰면 되고, b는 MDI 프로그램이라거나 자체 단축키가 있을 때, 그리고 c는 혹시 modeless 대화상자가 존재할 때에만 쓰면 된다.

본인이 개발하던 문제의 프로그램은 타 프로세스로부터 받은 메시지에 반응하여 대화상자를 표시하는 기능이 있었다. 그런데 만들어 놓고 보니 당장은 동작하는 것 같지만 뭔가 미묘하게 정상이 아닌 상태 같았다.
대화상자가 떠 있는 동안에도 우리 쪽에서 정의한 custom 메시지를 처리할 수 있게 하기 위해 번거롭지만 대화상자를 modal 대신 modeless 형태로 바꿨다.

그런데 tab을 눌러서 컨트롤간 포커스를 이동시켜 보면 이상한 비프음이 나면서 이동하고, Alt+Space로 시스템 메뉴를 꺼내서 창을 닫으면 제대로 동작하지 않고 프로그램 동작이 수 초간 멎는다거나..
owner(parent) 윈도우를 타 프로세스의 윈도우로 지정해서 그런가, 처음에는 쓸데없는 부분을 의심하며 한참을 헤맬 수밖에 없었다.

이것도 코드를 한참을 들여다 본 뒤에야 내가 무슨 삽질을 했는지를 깨닫고 경악하게 됐다. message loop을 이런 식으로 짰기 때문이었다.

while(GetMessage(&msg, NULL, 0, 0)>0) {
    for(HWND hDlg: m_hModelessDlgs)
        if(hDlg && ::IsDialogMessage(m_hDlg, &msg)) continue;
    ::TranslateMessage(&msg); ::DispatchMessage(&msg);
}

꺼내 놓을 수 있는 modeless 대화상자가 여러 종류가 추가되면서 나름 저렇게 for문을 넣었는데..
난 저 continue가 여전히 while문 전체를 제낄 수 있다고 생각했던 것이다.
그러니 대화상자로만 가야 할 메시지가 여전히 DispatchMessage로도 이중으로 전해지고 있었고, 이 때문에 뭔가 동작은 하는 것 같은데 잡다한 부작용까지 덩달아 발생하고 있었다.

내가 Windows 프로그래밍 경력은 10년을 훌쩍 넘기지만.. 그래도 정신이 없을 때는 아직까지 이런 초보적인 실수도 한다.

4. 대화상자를 종료하는 법

대화상자를 하나 구현했다.
사용자는 메뉴를 거쳐서 그 대화상자를 열고 닫는데, 그걸 한번 닫은 뒤부터는 메뉴를 선택해도 그 대화상자가 다시 열리지 않는 버그가 있었다. 그것도 언제나 그러는 것도 아니고 특정 기능을 사용해서 간접적인 방법으로 대화상자를 닫았을 때 문제가 발생했다.

디버깅 끝에 밝혀진 원인은 바로.. modeless 대화상자를 EndDialog 함수를 이용해서 닫았기 때문이었다.
modal 대화상자를 훗날 modeless 형태로 개조하면서 발생한 부작용의 연장선이다. 대화상자를 닫는 부분을 그에 맞게 제대로 고치지 않았었다.

EndDialog를 호출했더니 그 대화상자는 ShowWindow(hDlg, SW_HIDE)를 한 것처럼 화면에서 사라지기만 할 뿐, 실제로 파괴되고 없어지지는 않았다. modeless 대화상자는 여느 윈도우를 없앨 때처럼 DestroyWindow 함수를 직접 호출하든가 WM_CLOSE를 날려서 없애야 한다. 오오.. 이것도 의외로 자주 할 수 있는 실수로 보인다.

그럼 반대로 modal 대화상자의 프로시저에서 자기 자신을 DestroyWindow로 날려 버리면 어떤 일이 벌어질지가 궁금해진다. EndDialog와 달리 DestroyWindow는 IDOK나 IDCANCEL 같은 리턴값을 지정할 수 없다. 그렇기 때문에 이때 DialogBox 계열 함수의 리턴값은 그냥 '취소'를 눌러 종료된 것과 동급으로 취급되어야 하지 않나 싶다.

5. 맺는 말

이런 일련의 경험을 통해, "타 프로세스로부터 받은 메시지에 반응하여 대화상자를 표시하기" 이게 생각보다 더욱 기괴한 상황이란 걸 실감할 수 있었다.

프로세스 A가 프로세스 B로 메시지를 send로 보냈는데 B가 대화상자나 메시지 박스 같은 modal UI를 표시하고, 그게 종료된 뒤에야 return을 해 준다면.. 그 동안 프로세스 A는 자기 메시지를 처리하지 못하는 '응답 없음' block 상태가 돼 버린다. 특히 그 상태에서 B가 A로 역으로 메시지를 보내기라도 한다면 둘 다 꼼짝없이 dead lock에 빠진다.

메시지를 주고받는 주체와 객체가 다 동일 프로세스의 동일 스레드 소속이라면 걱정할 것 없다. 그 modal UI를 돌리는 B쪽의 메시지 loop에서 A에 소속된 윈도우에게 WM_PAINT 같은 메시지가 온 것도 같이 처리를 해 주기 때문이다. 하지만 프로세스, 아니 스레드 소속만 달라도 이런 일이 저절로 같이 행해지지 않는다.

그렇기 때문에 프로세스와 스레드 경계를 넘나들며 UI를 표시할 때는 대화상자는 최대한 modeless 형태로 만들고, 메시지도 post가 아니라 반드시 send로 보내야겠다면 ReplyMessage를 호출해서 "선응답 후UI" 형태로 프로그램 UI의 반응성을 보장해야 한다.

사실, 같은 프로세스이더라도 자기와 소속이 다른 스레드에 속한 윈도우로는 SetFocus를 할 수 없으며 심지어 DestroyWindow조차도 직통으로 먹히지 않는다. AttachThreadInput를 써서 입출력 연결을 한 뒤에나 포커스 지정이 가능하며, 윈도우의 파괴는 WM_CLOSE를 보내는 방식으로 간접적으로 해야 한다. WM_CLOSE는 WM_DESTROY와 달리, 자신이 파괴되는 것을 해당 윈도우 프로시저가 거부할 수 있다.

Posted by 사무엘

2018/05/26 08:34 2018/05/26 08:34
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1493

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/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:
3043946
Today:
1138
Yesterday:
2435