« Previous : 1 : ... 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : ... 31 : Next »

Windows 프로그래밍의 관점에서 볼 때 대화상자는 참 독특하면서도 중요한 위상을 차지하는 GUI 구성요소이다.
대화상자에는 누구나 공통으로 갖추고 있어야 하는 동작과 처리가 있으면서 한편으로 각종 자식 컨트롤로부터 notification을 처리하는 건 대화상자마다 제각각 customize가 가능해야 한다.

그래서 대화상자 함수는 customization을 위해 사용자가 작성한 대화상자 메시지 처리 콜백 함수를 인자로 받는다. 그리고 WM_SETICON 같은 메시지를 통해 아이콘조차도 클래스 차원이 아니라 윈도우 메시지 차원에서 필요하다면 변경할 수 있게 해 놓았다.
그럼 custom 프로시저가 가로채지 않은 나머지 공통 처리들은.. 마치 DefWindowProc처럼 DefDlgProc 같은 대화상자 윈도우 프로시저를 호출하는 것만으로 전부 가능해야 할 것 같지만, 사실은 그렇지 않다. Alt+단축키 처리, 그리고 포커스 이동 시에 '확인' 같은 default 버튼의 비주얼을 바꾸기 같은 것은 대화상자 윈도우 자체가 메시지를 받는 게 아니라 각 child 컨트롤들이 키보드 메시지를 받고 있을 때 그걸 가로채서 해치워야 한다. 대화상자 윈도우가 무슨 훅킹이라도 하고 있지 않은 이상 말이다.

이런 이유로 인해 대화상자의 동작을 위해서는 message loop 차원에서 메시지를 가로채는 로직이 필요하다. 그래서 IsDialogMessage라는 함수가 그 코드에 들어가야 한다. 물론 DialogBox 함수로 modal 대화상자를 만들었을 때는 해당 함수가 자체적으로 message loop을 돌리면서 그 처리를 당연히 알아서 해 주니, 저런 별도의 함수는 우리 쪽에서 modeless 대화상자를 운용할 때에나 필요하다.

사실 저 함수는 용도를 생각하면 Is..가 아니라 TranslateDialogMessage 정도로 작명이 됐어야 했다. 그래야 오해가 없다. 단순히 쿼리에 대한 판별값을 되돌리는 게 아니라 실제로 무슨 일을 수행하기 때문이다.

뭐, message loop은 그렇다 치고, 대화상자를 대상으로 생성된 메시지는 우리가 지정한 대화상자 프로시저 → 대화상자 자체의 윈도우 프로시저(DefDlgProc) → 운영체제가 제공하는 DefWindowProc 이런 순으로 메시지를 처리하게 된다. 앞 계층에서 처리하지 않은 메시지가 다음 계층으로 간다는 뜻이다.
C++이라면 이건 깔끔한 클래스의 상속 관계로 표현할 수 있을 것이다. 그러나 Windows가 처음 개발됐을 때는 C++이 쓰이지 않았었다. 그리고 결정적으로, 모종의 이유로 인해 대화상자 프로시저와 윈도우 프로시저는 형태가 미묘하게 서로 달라져서 온전한 일관성이 보장되지 않는다.

오리지널 윈도우 프로시저는 내가 메시지의 처리를 한다면 리턴값을 바로 되돌리면 되고, 내가 처리하지 않은 메시지에 대해서는 DefWindowProc()를 해 주면 된다.

return ret; //처리한 경우
return DefWindowProc(hWnd, msg, wParam, lParam); //처리하지 않은 경우

그러나 대화상자 프로시저는 자기가 이미 DefDlgProc라는 디폴트 처리 함수(= 대화상자의 원래 윈도우 프로시저)로부터 호출을 받은 구도이다. 이것이 디자인 상으로 가장 본질적인 차이점이다.
그래서 리턴값으로는 이 메시지를 우리가 처리했는지의 여부만을 BOOL 형태로 되돌리고, 메시지 리턴값은 따로 꽤 번거롭게 넣어야 한다.

SetWindowLongPtr(hDlg, DWLP_MSGRESULT, ret); return TRUE; //처리한 경우
return FALSE; //처리하지 않은 경우

저럴 거면 차라리 함수의 인자에다가 LRESULT *pnResult 같은 포인터를 추가해서 거기로 되돌릴 수 있게라도 하지 하는 아쉬움이 남는다.

*pnResult=ret; return TRUE; //처리한 경우 -- 대안 1
*pbEaten=TRUE; return ret; //처리한 경우 -- 대안 2

그런데, 대화상자 프로시저에도... 형태가 간단한 극소수의 예외적인 메시지는 리턴값을 SetWindowLongPtr이 아니라 일반 윈도우 프로시저처럼 자신의 리턴값으로 되돌린다. 그렇기 때문에 대화상자 프로시저는 대부분의 경우 TRUE/FALSE만을 되돌림에도 불구하고 리턴값이 쿨하게 BOOL이 아니라 INT_PTR로 지정되어 있다. LRESULT도 아니고 이거 참.. 지저분하다면 지저분하다.

그 예외로는 자식 컨트롤의 배경을 칠할 브러시를 되돌리는 WM_CTL*, owner-draw 컨트롤에서 아이템 간의 대소를 비교하는 WM_COMPAREITEM, 그리고 역시 owner-draw 컨트롤에서 아이템의 바로가기 key를 지정하는 WM_(CHAR/VKEY)TOITEM이 여기에 해당한다. COM 함수라고 해도 실패를 할 우려가 전혀 없고 리턴값도 BOOL 값 달랑 하나 같은 건 굳이 BOOL *pfResult라는 인자로 주기보다는 간단히 S_OK, S_FALSE라는 자기 리턴값으로 되돌리는 게 더 나은 것과 같은 이치이다.

그리고 저 예외 메시지들은 owner-draw처럼 애초에 custom 동작을 염두에 두고 만들어진 메시지이므로 default로 넘기느냐 마느냐를 따지는 게 전혀 무의미하다. 그러니 리턴값을 바로 직통으로 사용하는 것이 더 간편하고 낫다. WM_CTL*의 경우도 컨트롤을 서브클래싱할 때에나 쓰이니 이 역시 customization과 관계가 있다.

왜 저렇게 예외가 만들어졌는지 이제 이해는 되지만, 그래도 대화상자 프로시저의 인터페이스가 구리고 지저분하고 복잡해 보이는 건 어쩔 수 없는 사실이다. 그래서 대화상자 프로시저도 윈도우 프로시저와 비슷한 구조로 만들 수 있게 하는 일종의 코딩 디자인 템플릿이 있다. MFC 같은 C++ 프레임워크는 밑바닥에서 당연히 이런 일도 다 해 주고 있지만, C만 쓴다거나 MFC보다 더 가벼운 Windows API 프레임워크를 우리가 직접 만드는 상황이라면 그 내부 디테일을 알 필요가 있다.

기본적인 아이디어는 이렇다.
운영체제의 DefDlgProc는 우리 프로시저를 먼저 호출하여 메시지의 처리 여부를 묻는다. 우리 프로시저는 그 메시지를 처리한 경우라면 리턴값 + TRUE만 되돌리면 되니 끝이다. 그 반면 처리하지 않은 경우, 내부적인 재귀호출 플래그를 설정한 뒤 DefDlgProc을 또 호출한다.
그럼 DefDlgProc는 아까처럼 우리를 또 호출하는데, 이때 우리 프로시저는 이미 설정된 재귀호출 플래그를 보고는 비로소 더 처리를 하지 않는 FALSE를 되돌린다.

또한, 리턴값을 지정할 때 메시지 종류에 따라 곧이곧대로 리턴(일부 예외 메시지들)하거나 SetWindowLongPtr을 호출하는 것은 메시지 핸들러 말고 그 아래의 static 콜백 함수에서 하면 된다.
이 아이디어를 의사코드로 표현하면 다음과 같다.

class CMyDialog {
    static INT_PTR CALLBACK DialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
    BOOL m_bRecur;
protected:
    virtual LRESULT DlgProc(UINT msg, WPARAM wParam, LPARAM lParam);
public:
    CMyDialog(): m_bRecur(FALSE) { }
};

//클래스에서는 아마 private로 지정되어 있을 우리 대화상자 프로시저 callback.
INT_PTR CALLBACK CMyDialog::DialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    //HWND로부터 C++ 오브젝트 얻기. 훅킹을 해서 WM_NCCREATE를 잡든가 아니면 WM_INITDIALOG일 때
    //hDlg와 C++ 오브젝트를 연결하는 작업을 할 것. 이 코드에서는 그걸 생략함
    CMyDialog *obj = GetObject(hDlg);

    //BOOL값. 저 값이 true이면 재귀 상태라는 뜻이므로 그걸 false로 바꾸고, 함수도 false로 종료.
    CheckDefDlgRecursion(&obj->m_bRecur);
    //msg가 예외에 속하면 리턴값을 바로 되돌리고, 아니면 SetWindowLongPtr로 리턴값을 지정해 줌.
    return SetDlgMsgResult(hdlg, msg, obj->DlgProc(msg, wParam, lParam));
}

//각 클래스별로 오버라이드 하면 되는 가상 함수.
//윈도우 핸들은 우리 C++ 클래스의 멤버에 포함되어 있다고 가정함.
LRESULT CMyDialog::DlgProc(UINT msg, WPARAM wParam, LPARAM lParam)
{
    //재귀호출 플래그를 켠 뒤에 DefDlgProc를 호출한다.
    return DefDlgProcEx(m_hWnd, msg, wParam, lParam);
}

windowsx.h를 보면 SetDlgMsgResult, DefDlgProcEx, CheckDefDlgRecursion 같은 매크로 함수가 이런 디자인 패턴을 구현하라고 만들어진 물건들이다. 16비트 시절부터 있었고 MFC만큼이나 역사와 내력이 대단히 길다.

<날개셋> 한글 입력기는 지난 3.0때부터 GUI가 그냥 Windows API와 자체 제작 프레임워크를 사용해서 만들어졌지만, 그땐 본인은 이런 기법을 미처 생각하지 못했었다. 그래서 대화상자 프로시저들은 대충 return 1에 SetWindowLongPtr 등을 뒤죽박죽 섞어서 만들어져 있다. 그러나 저런 방법을 진작에 알았으면 대화상자 메시지 처리기도 마치 윈도우 프로시저 스타일로 일관성 있게 만들 수 있었겠다. 이제 와서 수많은 대화상자 프로시저들을 저 기준대로 고치는 것은 무의미한 지경이 됐지만 말이다.

Posted by 사무엘

2016/01/09 08:26 2016/01/09 08:26
, , ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1180

컴퓨터 소프트웨어에서 뭔가 기능은 동일하지만 프로토콜(입출력)이 다른 두 시스템을 최상위 계층에서 중재하여 서로 이어 주는 메커니즘을 '썽킹(thunking)'이라는 용어로 표현하는 것 같다. 영한사전에 제대로 등재돼 있지도 않은 신조어인데 정확한 어원이 궁금하다. 영문 위키백과는 a subroutine that is created, often automatically, to assist a call to another subroutine라고 풀이를 하며, 그래서

  • Windows 9x에서 유니코드(W) API 호출 요청이 왔을 때, 매개변수 값을 적당히 조절해서 그에 상응하는 Ansi API를 대신 호출하고 출력 결과도 wide string 기준으로 보정하는 것
  • C++의 멤버 함수 포인터에서 다중 상속된 클래스의 멤버 함수를 호출하기 전에 this 포인터의 오프셋을 보정해 주는 전처리 함수 (보정 정보가 포인터의 내부에 있지 않고 그냥 또 다른 함수를 생성해서 때우는 경우)

이런 것들도 다 넓게는 썽크/썽킹이라고 부른다. 그리고 썽킹을 수행하는 함수를 썽크 함수라고 한다. 다만 Windows에서 썽킹은 컴퓨터 아키텍처의 장벽을 극복하는 호환성 유지 작업을 가리킬 때 주로 쓰인다. 16비트와 32비트 사이, 그리고 요즘은 32비트와 64비트 사이에서 말이다.
그래서 그런 썽킹 시스템을 WoW라고 부른다. 월드 오브 워크래프트...가 아니라 Windows on Windows의 약자로, 말 그대로 Windows 위에서 Windows를 또 가상으로 구동한다는 뜻이다.

오늘날 우리가 사용하고 있는 x86-64 아키텍처는 과거 32비트 IA32의 superset 구도로 설계되었다. 그래서 32비트 x86 코드도 에뮬레이션이 아니라 네이티브 직통으로 실행 가능하기 때문에 Windows 입장에서는 소프트웨어적으로 32비트와 64비트를 모두 지원하기 위해 특별히 힘든 일을 해 줘야 할 건 없다. 64비트 시대에도 어마어마한 32비트 유물들은 비록 바이너리 형태가 아무리 지저분하더라도 결코 외면할 수 없는 물건임이 입증된 셈이다. 비록 성능을 추구했다고는 하지만 x86 코드를 거북이처럼 에뮬레이션으로 돌리고 다른 문제점도 많았던 IA64는 정말 시원하게 망해서 인텔의 흑역사가 됐다.;;

32비트와 64비트는 한 주소 공간 안에서 코드가 섞이는 게 전혀 불가능하며, 서로 교류하는 방법은 API나 커널 오브젝트 차원에서의 IPC 메커니즘밖에 선택의 여지가 없다. 특히 WM_COPYDATA 메시지는 16, 32, 64비트까지 세대를 넘어 두루 통용되는 대단히 훌륭한 해결사이다.
그러나 16비트와 32비트가 공존하던 시절에는 상황이 훨씬 더 지저분했다. 기존 소프트웨어와의 호환성, 그리고 부족한 PC 메모리 같은 현실적인 한계 때문이었다.

그 시절에는 기술적으로 다음과 같은 형용사가 붙은 썽킹들이 존재했다. 썽킹이 무슨 CPU 에뮬레이션까지 하는 건 물론 아니고, 주로 한 일은 최신 32비트 가상 메모리 주소와 구닥다리 16비트의 세그먼트-오프셋 주소를 그냥 상호 변환하는 것이었다.

1. Universal: 16비트 플랫폼에서 16비트 EXE가 32비트 DLL을 실행
이건 가장 원초적인 썽킹이며, 쉽게 말해 Win32s의 기술 수준이 여기에 해당한다.

2. Generic: 32비트 플랫폼에서 16비트 EXE가 32비트 DLL을 실행
이것은 32비트 Windows 9x/NT 계열이 모두 지원하는 썽크로, 방향이 universal과는 반대이다.
본인은 IME 개발자로서 이 썽킹의 존재를 자연스럽게 인지하고 있었다. 16비트 프로그램에서도 <날개셋> 한글 입력기 외부 모듈이 동작했기 때문이다. 물론 곧 오류가 나고 제대로 사용하기는 어렵지만.

Windows 9x의 경우, 16비트 프로세스 내부에서 돌아가는 32비트 코드는 마치 Win32s처럼 극도로 가난한 16비트 컴퓨터의 끔찍한 제약들이 고스란히 적용되었다. 스레드를 생성할 수 없으며 스택 크기도 기본 1MB 이상이 아니라 64KB 미만으로 팍 줄었다. 그러니 복잡한 재귀호출이나 큰 배열조차도 함부로 만들면 안 됐다.

Windows NT는 모든 운영체제의 코드가 32비트 이상이다. 하지만 그렇다고 해서 16비트 코드를 실행하는 기능 자체가 아예 없는 건 아니었으니 16비트 프로그램이 32비트 코드를 사용하기 위한 최소한의 썽킹 계층은 마련하고 있다.

16비트 EXE 안에서 동작하는 32비트 dll이 GetModuleFileName(NULL)을 해 보면 9x 계열의 경우, 16비트 EXE 이름이 아니라 그냥 kernel32.dll이 돌아온다. 그러나 NT 계열은 ntvdm.exe가 돌아온다. 16비트 코드는 아예 샌드박스 안에서 고립된 채 돌아가고, 얘와 연계하여 동작하는 32비트 DLL은 여전히 32비트 문맥이 완전히 보장된다.

3. Flat: 32비트 플랫폼에서 32비트 EXE가 16비트 DLL을 실행
이것은 Windows 9x에만 존재한다. 20여 년 전에 Windows 95가 나오고 제품의 32비트 에디션을 출시하긴 해야 하는데 방대한 16비트 기반 DLL 엔진 같은 걸 차마 다 32비트로 포팅할 시간이 없을 때, 차선책으로 쓰라고 도입된 솔루션이다.

Universal은 그냥 32비트로 빌드만 하면 혜택을 입을 수 있고 Generic도 해당 16비트 EXE에서 LoadLibrary32Ex32W나 CallProc32W 같은 썽킹 API만 새로 사용하면 썽킹이 가능한 반면, Flat 썽킹을 활용하려면 thunk 컴파일러를 이용해서 상호 교신하고자 하는 32/16비트 바이너리들에 썽크 중재용 DLL을 추가로 넣어 줘야 한다.

1~3은 성격과 용도가 제각각 다르다는 걸 알 수 있다.
과거에 아래아한글이 최초의 Windows 버전인 3.0이 개발되었을 때, 시행착오로 인해 universal 썽크만 생각했지 flat 썽크를 고려하지는 못한 듯하다. 그래서 내가 듣기로는 Windows 3.1 + win32s에서는 실행되었는데 정작, 곧 출시된 Windows 95에서는 어처구니없게도 동작하지 않았다고 한다. (NT는 모르겠음)

아래아한글이 100% 완벽하게 32비트로만 개발됐으면 이런 일이 없었을 텐데 아마 내부적으로 호환성 때문에 16비트 코드가 일부 존재했던 모양이다. 그리고 내가 들은 이런 소문들이 사실이라면, Universal 썽크는 Win32s + 32비트 EXE에서 도로 16비트 DLL을 로딩하는 것도 지원하긴 했나 보다. Windows 95에서 추가로 해야 하는 썽킹이 없이도 뭔가 되는 일이 있었으니까 말이다.
어쨌든 이 때문에 Windows 95가 발매된 1995년 말엔 그 문제를 해결한 3.0b가 신속하게 나와야 했다. 소문에 따르면 3.0a도 있었는데 이건 3.0 원판보다도 존재감이 "더" 없는 것 같다.

이런 썽킹은 커널 레벨에서 발생하기 때문에 뭔가 정보를 잘못 줘서 에러가 나더라도 애플리케이션 레벨에서는 디버깅조차 할 수 없다. 더 깊게 들어가지 않은 채로 그냥 에러가 난다는 뜻이다. 이런 건 커널 레벨 디버거를 써서 살펴봐야 한댄다.
비록 제대로 안 돌아가는 제품을 돈 주고 사서 쓰다 고생한 사용자들에게서 욕도 많이 먹었겠지만, 그래도 그 열악한 환경에서 그 방대한 도스용 프로그램을 그 제한된 시간과 예산 하에서 Windows용으로 뚝딱 포팅을 한 건 대단한 일이긴 해 보인다.

Windows 9x 시절에 썽킹과 관련된 대표적인 테크닉 중 하나는 32비트 프로그램이 지금 시스템에 남은 리소스 퍼센티지를 얻어 오는 것이었다. 그 값을 얻으려면 32비트 프로그램이 32비트 user32.dll이 아니라 16비트 user.exe의 함수를 호출해야 했기 때문에 일종의 flat 썽킹이 필요했다.

물론 운영체제가 제공하는 rsrc32.dll이 제공하는 함수를 호출하는 방법도 있지만(얘는 rsrc16.dll로 내려가고), 걔네들이 하는 일을 별도의 dll 없이 직통으로 수행하는 꼼수가 있는데.. 이건 분량이 길어지는 관계로 나중에 다시 다루도록 하겠다.
자, 그럼 썽킹과 관련된 다른 얘기도 슬슬 좀 꺼내 보겠다.

본인은 에디트, 리스트 같은 기성 컨트롤들에 보이는 명령 메시지들의 값이 16비트 시절과 32비트 이후 시절이 서로 다르다는 것을 비교적 최근에 알고서 놀랐다.
과거에는 LB_ADDSTRING, EM_SETSEL 같은 메시지들이 WM_USER 이후 영역에 있었다. 그러나 32비트에서는 이것들이 WM_USER 이내의 시스템 메시지 영역으로 옮겨졌다. 아마 대화상자 내부에서 다른 사용자 메시지들과의 충돌을 막으려는 의도도 있고, 또 얘는 WM_GET/SETTEXT처럼 프로세스 간에 문자열을 주고받을 때 운영체제가 자동으로 메모리 보정을 해 준다는 의미에서 시스템 메시지로 승격된 게 아닌가 싶다.

대화상자에서 어떤 컨트롤이 WM_GETDLGCODE 메시지에 대해 DLGC_HASSETSEL를 되돌리면 걔는 포커스를 받았을 때 텍스트 전체를 선택하라는 EM_SETSEL 메시지를 받는다. 자신이 에디트 컨트롤을 자체 구현하고 있다면 이 메시지를 저렇게 처리하면 된다.
그런데 Windows 9x에서는 저 메시지가 안 오고 WM_USER+1에 속하는 정체불명의 괴메시지가 오곤 했다. 알고 보니 저건 16비트 시절의 EM_SETSEL과 같은 값이었다.

이런 레거시들 사연들은 모를 때보다 알 때가 프로그래밍에 훨씬 더 도움이 된다. 물론 그렇다고 해도 32비트 프로그램에다가도 Windows 9x는 왜 16비트 기준의 메시지를 보내는지는 알 수 없는 일이다. Windows의 썽킹 계층은 메모리 주소/포인터 변환뿐만 아니라 GUI에서는 이런 메시지의 변환까지도 도맡아 했다고 한다.

지금은 공용 컨트롤들의 메시지가 WM_USER 밖의 영역에 있다. 얘들은 애초부터 프로세스간의 메모리 보호가 잘 되는 32비트 운영체제와 함께 등장하기도 했고 또 알다시피 복잡한 플래그들이 들어간 복잡한 구조체를 주고받다 보니, 운영체제가 inter-process간에도 메모리 보정을 해 주지 않는다.
다른 프로세스에 있는 리스트/트리 컨트롤에다가 데이터를 등록하려면 얄짤없이 훅 프로시저를 써서 그 프로세스의 문맥 안에서 해당 컨트롤을 조작해야 한다. 사실, 남의 프로세스의 GUI 컨트롤을 조작하는 변태적인 작업이 왜 필요한지는 모르겠지만 말이다.

잘 알다시피 16비트 시절엔 DLL 전역변수들이 한데 공유됐으며, 제2, 제3 EXE들이 연결됐을 때 PROCESS_ATTACH 통지가 없었다고 한다. 그러니 DLL 함수는 exe들이 자기 DLL의 context를 매번 함수 인자로 전해 줘야 했겠다. 초기화/해제도 당연히 수동으로 직접 해야 했으나.. crash가 발생해서 해제를 제대로 못 하고 레퍼런스 카운트가 꼬이면 그건 그대로 시스템 자원 누수로 이어졌다.

요즘은 레퍼런스 카운트 관리가 엉망이더라도 프로세스가 종료되면 그 프로세스가 갖고 있던 자원은 100% 회수되는 게 보장된다. 그리고 요즘은 실행이 강제 종료 당한 스레드에서 스택 메모리가 자동 회수되지 않는 것 정도만이 그나마 in-process leak인 반면, 그 시절엔 툭하면 시스템 차원에서의 자원 누수와 고갈을 아주 쉽게 야기할 수 있었다. Windows 9x는 도스와 시스템 영역 메모리가 보호되지 않는 것 정도 때문에만 불안정했지만 3.1은 이보다 훨씬 더 막장이었다.

컴퓨터 환경을 더 좋게 만들기 위한 노력은 단순히 물리적으로 회로 집적도를 높이는 것만으로 되는 게 아니며 그 자원을 효율적으로 활용할 수 있게 소프트웨어적으로도 머리를 엄청 많이 써야 했다는 걸 알 수 있다.

Posted by 사무엘

2016/01/06 08:36 2016/01/06 08:36
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1179

Windows에서 응용 프로그램의 창을 최대화하면 그 창은 화면에 말 그대로 꽉 차서 자신 주변의 테두리는 보이지 않게 된다. 이렇게 말이다.

사용자 삽입 이미지

그런데 머언 옛날, Windows 95에서는 일부 프로그램이 그렇게 일반적인 형태로 최대화가 되지 않았다. 테두리가 화면의 밖으로 밀려난 게 아니라 화면에 “포함된” 형태로 최대화되곤 했다. 그림판, 그리고 Internet Explorer 2가 대표적인 예였다. 너무 옛날 운영체제와 옛날 프로그램이어서 기억이나 실감을 못 하는 분도 계시겠지만 실제로 그랬다.

사용자 삽입 이미지

도대체 이 프로그램들은 왜 이렇게 동작했던 것일까? 그리고 어떻게 하면 이런 동작을 구현할 수 있을까?

Windows에는 창의 크기 내지 최대화 상태와 관련된 동작을 제어하는 WM_GETMINMAXINFO라는 요긴한 메시지가 있다. 이 메시지를 통해 이 창의 최소 및 최대 크기를 지정할 수 있으며, 창이 최대화되었을 때에 화면을 차지할 영역 범위도 지정할 수 있다.

크기 조절이 가능한 대화상자를 만들었는데 특정 한계 이하로는 가로나 세로 크기가 더 줄어들지 않게 하고 싶을 때, 그리고 가로로만 키울 수 있고 세로로는 더 키울 수 없게 하고 싶을 때 이 메시지를 처리하면 된다.

요즘은 좀 드물어졌지만 옛날에 Visual Basic 4라든가 델파이 같은 RAD 툴은 프로그램의 메인 윈도우 자체는 메뉴와 도구상자, 컴포넌트 팔레트 같은 것만 있었다. 폼 디자이너나 코딩 에디터는 메인 윈도우와 대등한 위상인 별도의 창으로 존재했다. 이 메인 윈도우는 가로로만 크기 조절이 되지 세로로는 되지 않았다.

사용자 삽입 이미지

그리고 과거의 ‘매체 재생기’ 역시 동영상은 별도의 창으로 출력됐지 메인 윈도우에는 메뉴와 위치 슬라이더, 재생 버튼 같은 것만 있었기 때문에 크기 조절은 가로로만 가능했다.
이런 프로그램들은 최대화가 가능하지 않거나, 최대화를 하더라도 가로로만 최대화가 됐다. 이런 동작을 위해서 WM_GETMINMAXINFO에다 크기 한계치를 지정해 주면 된다.

또한 이 메시지는 한계를 지정하는 것뿐만 아니라 한계를 초월하는 창 크기를 지정할 때도 쓰인다. ‘전체 화면’ 기능을 구현하는 게 대표적인 예이다.

위와 같은 활용을 하기 위해서는 메시지와 함께 전달된 MINMAXINFO 구조체에서 ptMaxSize, ptMinTrackSize, ptMaxTrackSize의 값을 고치면 된다. 다만 이들은 개념적으로 POINT(x, y)가 아니라 SIZE(cx, cy)에 해당하는 값인데 왜 구조체를 POINT로 지정했는지는 개인적으로 모르겠다. 시각과 시간을 헷갈린 것과 비슷한 격이다.

자, 그럼 크기 조절이 아니라 처음 주제인 최대화 이야기로 돌아온다. Windows 95의 그림판이나 IE2와 같은 동작을 하려면 ptMaxPosition이라는 멤버의 값을 고치면 된다. 창이 최대화됐을 때 이 창이 있을 곳을 지정하는 정보이므로 얘는 SIZE가 아닌 POINT의 정의에 부합하며, 디폴트로는 테두리를 가리기 위해서 화면을 살짝 벗어난 음수값이 설정되어 있다. 이것을 (0, 0)으로 설정하면 테두리가 화면에 보이게 된다.

MINMAXINFO *lpMMI = reinterpret_cast<MINMAXINFO *>(lParam);
lpMMI->ptMaxSize.x += lpMMI->ptMaxPosition.x*2;
lpMMI->ptMaxSize.y += lpMMI->ptMaxPosition.y*2;
lpMMI->ptMaxTrackSize=lpMMI->ptMaxSize;
lpMMI->ptMaxPosition.x=lpMMI->ptMaxPosition.y=0;

그런데 놀랍게도 여기에는 반전이 있다.
95 이후 오늘날의 Windows에서는 ptMaxPosition의 x, y 값을 모두 0으로 지정하면 값이 인식되지 않는다. 테두리가 밖으로 가려지는 디폴트 방식으로 창이 최대화된다. (0, 0)일 때만 의도적으로 보정을 한다는 것은 (0, 1), (1, 0) 이나 (-1, -1) 같은 비슷한 값을 줘 보면 금방 눈치챌 수 있다.

오로지 Windows 95만이 (0, 0)을 주면 창의 최상단 좌측 꼭지점이 말 그대로 (0, 0)으로 잡힌다. 이것은 본인이 프로그램을 직접 작성해서 돌려 보면서 확인한 사항이다.

이런 이유 때문인지, Windows 98(그 이후도 물론 포함)의 그림판은 95와 외형과 기능의 차이가 거의 없음에도 불구하고 최대화했을 때 95처럼 테두리가 보이는 형태로 커지지 않는다.
쉽게 말해 저건 오로지 95에서만 볼 수 있었던 추억의 특이한 동작인 것이다. NT4는 사정이 어떠했나 모르겠다.

사용자 삽입 이미지

WM_GETMINMAXINFO는 DefWindowProc의 영향을 받지 않는다는 점을 참고하도록 하자.
우리가 이 메시지를 받은 순간부터 lParam이 가리키는 MINMAXINFO 구조체에는 디폴트 값들이 이미 들어있으며, 우리는 필요한 경우 이 값들을 고치기만 하면 된다.
DefWindowProc가 구조체에다 디폴트 값을 넣어 준다거나 하는 건 없기 때문에 이 메시지는 그리로 전달을 하건 말건 동작이 달라지지 않는다.

훗날 Windows XP에서는 응답이 없이 죽어 버린 프로그램 창에 대해서 최소한의 반응성을 보장하기 위해 ghost 윈도우라는 걸 도입했다. 그런데 최대화된 상태에서 고스트가 됐다가 다시 살아난 윈도우는 최대화 이전 상태의 크기 정보가 사라져서 딱 저 95처럼 화면에 테두리가 보이는 최대 크기로 바뀌는 버그가 있었다. 물론 sp1 무렵에 곧바로 고쳐지긴 했다.

Posted by 사무엘

2015/12/28 19:37 2015/12/28 19:37
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1176

컴파일러의 경고 외

군대 유머, 관제탑 유머가 있는 것처럼 변호사를 소재로 한 블랙코미디 시리즈가 있다.
돈만 주면 자기 양심과 혼까지 팔아서 온갖 미사여구와 궤변(?)으로 범죄자의 형량을 감소시키고 심지어 무죄로 조작한다는.. 변호사에 대한 좀 과장되고 왜곡된 이미지가 들어가 있다.

천당과 지옥(혹은 천사와 악마)이 법정에서 소송이 붙으면 천당/천사 진영은 아마 승산이 없을 거라는 개드립조차 있다. 왜냐하면 유능한(=타락한-_-) 변호사들은 몽땅 지옥에 가 있어서 다 악마 편이기 때문에. -_-;; 물론 영적 법정에서 실제로 하나님이 어떤 편인지를 안다면, 그리고 성경에서 judgment라든가 judge라는 단어의 용례만 쭉 뽑아 보면 개드립은 그냥 개드립일 뿐이라는 걸 알 수 있다.

그런데 변호사가 굉장히 바보 같은 질문을 할 때가 있(었)는가 보다. 예를 들어..

  • 그림을 도둑맞던 당시에 선생님/고객님은 현장에 계셨습니까?
  • 그 일을 혼자 하셨나요? 아니면 단독 범행?
  • 충돌 당시에 두 차가 얼마나 떨어져 있었죠?
  • 그 스무 살 먹었다는 막내아들이 나이가 어떻게 된댔죠?
  • 전쟁에서 죽었다는 사람이 당신이었습니까, 아니면 당신 동생이었습니까?
  • 건망증을 앓고 계셨다면, 그럼 그 동안 잊어버린 것들의 예를 좀 들어 주시죠.

도대체 저 변호사 양반이 왕년에 그 무시무시한 사법 시험을 어떻게 통과했는지, 아니면 악착같은 공부 기계 괴수들이 몰리는 로스쿨을 어떻게 들어가서 졸업했고 어떻게 변호사 시험을 합격했는지를 의심케 하는 대목이 아닐 수 없다.

바보같은 질문은 그 변호사가 너무 격무에 시달린 나머지 (1) 정말로 뇌에 나사가 좀 풀려서 감을 잃었거나, (2) 정신 없어서 의뢰인을 완전 성의없게 대해서 나올 수 있다. 하지만 한편으로는 (3) 일부러 바보 같은 질문을 던져서 일종의 심문을 하려는 의도도 있다. 같은 내용을 비비 꼰 바보 같은 질문에 낚여서 진지하게 대답하다 보면, 일관성 없는 진술이 들통날 수 있기 때문이다.

뭐, 여기서 내가 법조인들의 심리나 심문 기법 같은 걸 얘기하려는 건 아니고.
중요한 건, 자연 언어뿐만 아니라 프로그램 코드도 사람이 작성하는 것이다 보니 저런 바보 같은 문장이 있을 수 있다는 것이다. 그리고 컴파일러가 그걸 지적해 주는 것을 우리는 '경고'라고 부른다.

간단한 예로는 선언만 해 놓고 사용하지 않은 변수, 초기화하지 않고 곧장 참조하는 변수, 한쪽에서는 class로 선언했는데 나중에 몸체를 정의할 때는 동일 명칭을 struct로 규정한 것이 있다. 딱히 에러까지는 아니고 코드 생성이 가능하지만, "혹시 다른 걸 의도한 게 아니었는지" 의심할 만한 부분이다.

더 똑똑한 컴파일러는 세미콜론이나 =/==사용이 아리까리해 보이는 것도 경고로 찍으며, 이런 것도 지적해 준다.
unsigned long p; (...) if(p<0) { }

unsigned 타입의 변수를 보고 "너 혹시 0보다 작니?"라고 묻는 건 그야말로 변호사가 "당신과 당신 동생 중 전쟁에서 죽은 사람이 누구라고 했죠?"라고 묻는 것이나 다름없다. 그러니 저 if 안에 있는 코드는 unreachable이라고 지적해 주는 건 적절한 조치이다.

사실, 사람이라 해도 처음부터 대놓고 저렇게 바보 같은 문장을 작성하는 경우는 드물다. 작성한 지 오래 된 코드를 나중에 리팩터링이나 다른 수정을 하게 됐는데, 같이 고쳐져야 하는 문장이 일부만 고쳐져서 일관성이 깨지는 경우가 더 많다. 남이 int를 기준으로 작성해 놓은 코드를 나중에 후임이 UINT로 고치면서 저 if문의 존재를 잊어버린다거나(알고 보니 이 값에 음수가 들어오거나 쓰일 일은 절대 없더라). 버그도 이런 식으로 생기곤 한다.

비주얼 C++에서 경고는 총 4단계가 있다. 1단계는 정말로 말이 안 되어 보이는 것만 출력하고, 4단계까지 가면 정말 미주알고주알 별걸 다 의심스럽다고 지적한다. 경고들을 그렇게 여러 단계로 분류한 기준은 딱히 표준이 있지는 않고 그냥 컴파일러 제조사의 임의 재량인 것으로 보인다.

비주얼 C++이 프로젝트를 만들 때 지정하는 디폴트는 3단계이다. 3단계를 기준으로 깔끔하게 컴파일되게 작성하던 코드를 4단계로 바꿔서 빌드해 보면 이름 없는 구조체를 포함해서 사용되지 않은 '함수 인자'들까지 온통 경고로 뜨기 때문에 output란이 꽤 지저분해진다. 물론, 특정 경고를 그냥 꺼 버리는 #pragma warning 지시문도 있지만, 그 자체가 소스 코드를 지저분하게 만드는 일이기도 하고.

그러니 어지간하면 3단계만으로 충분하지만, 4단계 경고 중에도 컴파일러가 잡아 주면 도움이 되겠다 싶은 일관성 미스 같은 것들이 있다.
그래서 모든 사람들이 코드의 모든 구조를 알지 못하는 공동 작업을 하는 경우.. (직감보다 시스템이 차지하는 비중이 더 커짐) 그리고 팀원/팀장 중에 좀 결벽증 강박관념이 있는 사람이 있는 경우, 4단계를 기준으로 프로젝트가 진행되며, 커밋하는 코드는 반드시 경고와 에러가 하나도 없어야 한다고 못을 박곤 한다. 심지어 경고도 에러와 동등하게 간주시켜서 빌드를 더 진행되지 않게 하는 컴파일 옵션을 사용하기도 한다.

변호사 유머를 보니까 컴파일러의 경고가 생각이 나서 글을 썼다.
내 생각엔 a=a++처럼 이식성 문제가 있고 컴파일러 구현체마다 다른 결과가 나올 수 있는 코드에 대해서나 경고가 좀 나와 줬으면 좋겠다. 저것도 초기화되지 않은 변수만큼이나 문제가 될 수 있기 때문이다. 비주얼 C++의 경고 level 4 옵션으로도 저건 그냥 넘어가는 듯하다.

예전에도 얘기한 적이 있듯, 법은 사람을 제어하는 일종의 선언/논리형 프로그래밍 언어로서 컴퓨터 사고방식으로 생각할 수도 있는 물건이다. 또한, 프로그램의 버전을 얼마나 올릴지 결정하는 게 형벌의 양형 기준과 비슷한 구석이 있다.
잡다한 기능들을 많이 추가한 것, 짧고 굵직한 기능을 구현한 것, 비록 작업량은 별로 많지 않지만 현실에서의 상징성과 의미가 굉장히 큰 것, 아니면 그냥 적당히 시간이 많이 흘렀기 때문에 숫자를 팍 올리는 것.

형벌이라는 것도 사람을 n명 죽인 것에 비례해서 징역이 올라가는 그런 관계는 당연히 아닐 테니, 상당히 많은 변수들이 감안된다.
이런 것들을 다 감안해서 다음 버전의 숫자를 결정하는데 이거 굉장히 복잡하다.

Posted by 사무엘

2015/12/10 08:33 2015/12/10 08:33
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1169

Windows의 공용 컨트롤 열전.
날짜/시간 컨트롤에 이어 오늘은 툴바(toolbar)라고 불리는 '도구상자/도구모음줄' 컨트롤에 대해서 좀 얘기를 해 보겠다.

메모장 같은 급의 초간단 프로그램이 아닌 이상, 표준 GUI 기반인 대다수의 프로그램들은 상단에 클릭 가능한 그림 버튼들이 가로로 쭉 늘어서 있다. 도구모음줄은 바로 그 그림 버튼들의 표시를 책임진다. 각 그림 내지 아이콘은 그 프로그램에서 자주 쓰이는 명령들을 나타내며, 이를 클릭하면 일일이 메뉴를 열어서 글자 형태로 된 명령문을 읽고 선택하는 것보다 명령을 더 빠르게 내릴 수 있다.
자주 쓰이는 명령에 대해서 키보드에 단축키가 있다면 마우스에는 도구모음줄이 있는 셈이다.

사용자 삽입 이미지

사실, 도구모음줄은 컴퓨터의 성능 관점에서는 그렇게 효율적인 도구가 아닐지도 모른다. 요즘이야 제약이 덜해지긴 했지만, 과거에 화면 해상도가 충분하지 못하던 시절에는 텍스트나 그림 같은 문서 컨텐츠를 한 줄 표시할 공간이 아까운데 도구모음줄을 늘어놓는 건 화면 낭비였다. 수십 종류에 달하는 명령 아이콘들도 다 메모리를 수십 KB 이상씩 잡아먹는다는 건 역시 두 말할 나위가 없고.

하지만 어떤 프로그램을 실행했더니 그냥 빈 화면에 cursor만 달랑 깜빡이는 것보다는, 아무래도 컬러풀한 아이콘들이 가득한 도구모음줄 하나라도 좀 놓여 있는 게 사용자에게 친근하다. 이것이 마우스 사용자에게는 뭔가 클릭할 거리를 제공함으로써 프로그램을 더 편리하게 사용하는 데 실질적인 도움이 된다.

1990년대 초, Windows 3.x 시절에도 MS Word나 Excel의 까마득한 옛날 버전을 보면 파일, 편집, 보기 같은 자주 쓰인 명령이 등재된 도구모음줄이 있었다. 도구모음줄이라는 개념은 그때 처음으로 등장한 것으로 보인다. 그 시절에는 도구모음줄은 자체 구현이었고 MFC조차도 그걸 자체 구현해 줬었는데, Windows 95로 넘어오면서 운영체제의 공용 컨트롤로 형태가 바뀌었다. MFC의 ToolBar 클래스도 4.0 32비트 버전부터는 운영체제가 제공하는 놈을 쓰는 걸로 형태가 바뀌었다.

그리고 1990년대 말, MS Office 97부터는 버튼의 모양이 마우스로 가리키고 있는 놈만 얇은 입체 테두리가 나타나는 flat 스타일로 바뀌었으며, 메뉴도 도구모음줄 버튼이 있는 명령은 왼쪽에 그 도구모음줄 아이콘이 같이 뜨게 되었다. 이건 당시로서는 나름 굉장히 참신한 디자인이었다.
Office야 운영체제의 표준 GUI를 안 쓰는 걸로 악명(?)이 높았으니, flat 스타일은 Windows 98 타이밍 때 공용 컨트롤에도 도입되었다.

사용자 삽입 이미지

운영체제 차원에서 공용 컨트롤이 등장했으니 Visual C++과 MFC가 독자적으로 하는 일은 이제 없어졌느냐 하면 여전히 그렇지 않다. MFC가 하는 일은 다음과 같다.
(1) 먼저, 도구모음줄의 컨테이너격인 Control bar를 제공한다. 도구모음줄의 폭을 자유롭게 지정하고 위치도 자유롭게 옮기고 심지어 부모 윈도우의 상하좌우 등 어디든 자유롭게 붙이거나 떼는 것은 운영체제가 알아서 해 주는 일이 아니다. MFC의 도움 없이 직접 구현하는 건 머리에 쥐가 나는 노가다이다. 심지어 드래그하기 편하게 왼쪽에 그려 주는 gripper 세로줄 공간도 MFC가 그려 준 결과물이다.

개발하는 프로그램이 덩치가 MS 오피스 내지 포토샵 같은 상업용 프로그램 급으로 커지면 도구모음줄이 2개 이상 존재하게 된다. 보기 메뉴의 '도구모음줄' 항목은 체크 하나만 달랑 있는 게 아니라 '표준, 서식, 그리기' 등 도구모음줄의 종류를 가리키는 부메뉴를 갖게 된다.
도구모음줄이 하나밖에 없을 때는 겨우 그것만 이리저리 옮기고 붙였다 떼는 기능이 좀 잉여스럽게 느껴지겠지만, 그게 여러 개가 존재하게 되면 이들의 위치를 관리하는 기능은 필수가 된다. MFC는 그런 필수 기능을 구현해 준다.

도구모음줄이 한두 개도 아니고 무려 10~20개씩 달려 있는 방대한 프로그램은 도구모음줄의 버튼들을 사용자 정의(customize)하는 기능도 전문적으로 갖추고 있다. 공용 컨트롤이 기본으로 제공하는 customize 기능도 있지만, 그건 전체 아이콘들 집합에서 자기 도구모음줄에다가 추가할 버튼을 선택하고 순서를 바꾸는 것 정도가 전부이다. 그 반면 MS Office의 경우, 2007 이전 버전은 메뉴의 텍스트, 도구모음줄의 버튼 그림까지 전부 사용자가 바꿀 수 있어서 가히 개발자의 근성을 짐작케 하는 엄청난 customize 기능을 제공했다. 나중에는 MS Office는 리본 UI 기반으로 바뀌고 Visual Studio도 WPF 기반으로 UI가 싹 바뀌면서 이런 기능은 더 찾아보기 어렵게 됐다.

이런 컨테이너 기능 말고도 또 Visual C++가 MFC와 연계하여 제공하는 기능은 바로 (2) IDE가 제공하는 리소스 편집기이다.
MFC로 응용 프로그램을 만든다면 우리는 리소스 편집기를 이용해서 리소스에다가 Toolbar를 추가하고 도구 버튼과 아이콘, 연계 명령들을 넣곤 한다. 그런데 이 Toolbar라는 리소스 카테고리는 Windows가 제공하는 표준 리소스 포맷이 아니다. 비트맵, 아이콘, 메뉴, 문자열과는 달리 표준 포맷이 아니며, MFC가 자체적으로 정의해서 사용하는 포맷이다. 여기에 지정된 데이터를 바탕으로 도구모음줄을 초기화하는 것은 응당 MFC의 몫이다. LoadIcon, LoadMenu, LoadString 따위와는 달리, LoadToolBar는 MFC 클래스의 멤버 함수로나 존재하지 Windows API에는 없다.

게다가 이 toolbar 리소스는 단독으로 있는 것도 아니다. 얘가 정의하는 것은 한 도구모음줄에 몇 개의 버튼이 있고 각 버튼이 의미하는 명령 ID는 무엇인지, 혹은 이것이 구분자인지 같은 정보가 전부이다. 그 도구모음줄이 참조하는 비트맵은 같은 ID의 Bitmap 리소스에 있다.
하지만 Visual C++ IDE는 도구모음줄과 연계하는 비트맵은 비록 비트맵이라 할지라도 표준 비트맵 리소스에서 따로 표시를 하지 않으며, 그 비트맵은 도구모음줄 리소스를 편집하는 곳에서 버튼 구조와 함께 편집하게 돼 있다. 프로그램이 내부적으로 이런 보정 처리까지 하고 있는 것이다.

내부적으로 MFC는 한 프로그램 윈도우에 대해서 한 리소스 ID를 부여하여 이걸로 문자열(프로그램 제목), 아이콘, 액셀러레이터 단축키, 표준 도구모음줄(비트맵 포함)까지 한데 관리를 하기까지 한다. 이것이 바로 CFrameWnd::LoadFrame 함수가 하는 일이다. 참 대단한 발상이다.

다음으로, 도구모음줄에 대해서 프로그래머가 반드시 짚고 넘어가야 할 기술적인 사항이 하나 있다.
도구모음줄의 버튼 그림은 작고 아담한 게 아이콘을 닮았지만, 실제로 이건 아이콘이나 마우스 포인터와는 달리 그냥 비트맵이다.
'아이콘'은 이미지 비트맵과 마스크 비트맵으로 구성되어서 태생적으로 래스터 오퍼레이션을 통해 투명 배경이나 반전 같은 걸 표현할 수 있다. 그러나 이미지 비트맵 한 장만 갖고는 그런 걸 표현할 수 없다. 그렇다면 도구모음줄 버튼은 어떤 방식으로 투명색을 표현하는 걸까?

이 방식은 생각보다 굉장히 원시적이고 단순무식하다. TB_ADDBITMAP 메시지라는 재래식 방식을 쓰는 경우, 도구모음줄은 이미지 비트맵 한 장만 달랑 받아들이고 투명 처리 같은 걸 해 주지 않는다. 비트맵을 생으로 있는 그대로 출력만 한다.
그렇기 때문에 MFC의 도구모음줄 클래스는 자신의 리소스 비트맵 이미지에 대해서 보정을 한다. 비트맵에 RGB(192,192,192)에 속하는 은색/회색 픽셀이 있으면 그걸 현재 운영체제의 COLOR_BTNFACE 시스템 컬러로 바꾸고, 은색 말고도 짙은 회색이나 검정, 하양은 그에 상응하는 시스템 컬러로 바꾼다. 그렇게 보정된 비트맵을 도구모음줄로 보내서 은색이 편의상 투명 배경색인 것처럼 보이게 한다. 보정을 안 하면 바로 이런 꼴 난다..;;

사용자 삽입 이미지

시스템 색상이 바뀌어서 WM_SYSCOLORCHANGE 메시지가 오면? 당연히 도구모음줄 비트맵도 매번 다시 만들어서 지정한다.
MFC를 뒤져 보면 이 일을 하는 AfxLoadSysColorBitmap라는 함수가 bartool.cpp에 있다. 아니, 이렇게 색깔 치환을 한 비트맵을 생성하는 함수가 Windows 95 시절부터 comctl32.dll에 CreateMappedBitmap이라고 있어 왔다. 도구모음줄 전용이기 때문에 user32도, gdi32도 아닌 comctl32에 있는 것이다.

그리고 이런 색깔 보정이 마냥 삽질인 것만은 아닌 것이..
시스템 색상이 고대비 검정 같은 걸로 바뀌었을 때는 검은색을 흰색으로 바꾸는 작업도 어차피 필요하기 때문이다. 도구모음줄의 비트맵은 반쯤은 이런 유동적인 환경 변화에도 대비가 돼 있어야 한다는 점이 흥미롭다.

도구모음줄용 비트맵으로 원시적인 생 비트맵뿐만 아니라 image list를 지정하는 TB_SETIMAGELIST 메시지는 Internet Explorer 4는 아니고 3과 함께 약간 나중에 추가됐다.
image list는 자체적으로 마스크 정보도 포함할 수 있으니 예전보다는 상황이 좀 낫다. 또한 ImageList_LoadImage 함수는 은색이 아닌 임의의 색깔을 투명색으로 지정할 수 있고, 아예 default로 (0,0) 화면 최상단 좌측 픽셀을 투명색으로 지정하게 할 수도 있다.
평소에는 흑백 이미지이다가 마우스 포인터가 가리키고 있는 버튼만 컬러 이미지로 출력하는 일명 hot image를 지정하는 것은 이렇게 image list 형태로만 지정 가능하다.

이렇게 특정 한 색깔을 투명색으로 끌어다 쓰는 건 아무래도 16색/256색 시절의 그래픽 패러다임을 벗어나지 못한 발상이다. MFC feature pack을 이용해서 트루컬러 아이콘이 들어간 도구모음줄을 만들면 당장은 보기 좋지만 시스템 색상을 고대비 검정으로 바꿔 보면 경계가 완전 뭉개지고 보기 좋지 않은 걸 알 수 있다. 어느 배경색에도 경계가 부드럽게 나오려면 근본적으로 알파채널이 쓰여야 할 텐데 그렇지 못하기 때문이다.

요컨대 오늘날 도구모음줄은 아이콘들이 그런 것처럼 트루컬러와 알파 채널에 대비가 돼 있어야 하고, 그러면서 고대비 모드도 지원해야 하며, 더 욕심을 부리자면 고해상도 DPI에서는 약간 큰 비트맵도 준비돼 있어야 한다. MS Office의 리본 UI는 고대비 모드에서는 그냥 16컬러로 간략화한 비트맵을 대신 출력하는 것 같다.
그리고 <날개셋> 편집기는 겨우 그 덩치의 프로그램에서 저런 것까지 일일이 대비하는 건 너무 낭비라 여겨지기 때문에 도구모음줄의 아이콘은 그냥 16*16 크기의 16색에 머물러 있다.. ^^

그나저나,

  • 공용 컨트롤을 쓰지 않고 자체 구현된 도구모음줄은 대화상자를 띄우는 명령을 클릭한 경우, 대화상자가 떠 있는 동안에 버튼이 여전히 눌러져 있기도 했던 것 같다. MS Office 2007 이전의 옛 버전과 Visual C++ 6 등이 그 예다.
  • 전무후무하게 도구모음줄 배경에 solid color가 아니라 무늬가 있었던 IE 3의 도구모음줄은 어떻게 구현되었는지가 새삼 궁금해진다. 얘는 표준 공용 컨트롤 기반인데, 아마 얘 때문에 image list 기능이 도입되었지 싶다. 이쯤 되면 버튼 비트맵에 자체적으로 마스크 정보가 있어야만 도구모음줄 배경과도 합성이 가능하지 않았겠는가.

사용자 삽입 이미지

사용자 삽입 이미지

Posted by 사무엘

2015/11/16 08:36 2015/11/16 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1160

컴퓨터에는 자체적으로 달력과 시계 기능이 포함되어 있으며, 운영체제에는 그걸 설정하는 기능이 있다.
대략 197, 80년대 엄청 미개하던 시절에는 개인용 컴퓨터의 시계에 배터리가 없었다. 그래서 컴퓨터가 꺼지면 현재의 날짜· 시각 정보가 사라졌으며, 매번 부팅 때마다 사용자가 그걸 다시 지정해 줘야 했다. 옛날에 도스가 autoexec.bat가 없으면 디폴트로 하던 일이 바로 date, time 명령을 실행하여 날짜· 시각을 입력받는 것일 정도였다.

그 반면 지금은 컴퓨터가 자체적으로 날짜· 시각을 관리하고 있을 뿐만 아니라 Windows의 경우 XP부터는 인터넷 서버로부터 시각을 자동으로 동기화하는 기능이 추가됐다. 그러니 긴 시간이 흐른 뒤에 시계가 오차가 쌓여서 부정확해질 일이 없어졌다. 기지국으로부터 받은 시각을 표시하는 휴대전화처럼 말이다. 다만, 휴대전화는 자체 시계는 없는지, 기지국과의 통신이 끊어지면 컴퓨터와는 달리 시각을 아예 표시를 못 하는 것 같다.

XP를 넘어 Windows Vista부터는 날짜· 시각을 사용자가 변경하는 건 관리자 권한이 필요한 일로 바뀌었다. 보안과 권한 개념이 발달해 있는 유닉스 계열 컴퓨터에서는 진작부터 그랬다.
거기에다 자동 동기화 기능까지 있으니 일반인이 컴퓨터에서 날짜· 시각을 손수 건드릴 일은 거의 없어졌으며, 소프트웨어의 사용 기한 같은 걸 속이려고 컴퓨터의 날짜를 조작하는 일도 예전보다는 쉽지 않게 됐다.

Vista 이전의 과거엔 Windows에서 작업 표시줄의 시계를 더블 클릭하면 곧장 날짜· 시각을 입력받는 제어판 대화상자가 떴다. 그러나 지금은 간단하게 달력과 아날로그 시계만 뜬 뒤, 날짜· 시각을 바꾸는 대화상자는 추가로 버튼을 클릭해야만 나오게 바뀌었다.

사용자 삽입 이미지

이 창은 정식 대화상자가 아니며, 마우스로 이동을 시킬 수가 없고 바깥을 툭 건드리기만 하면 바로 사라진다. 그렇기 때문에 예전에 비해 UI가 좀 불편해진 면모도 있다. 하지만 read-only 형태의 달력· 시계 UI만 먼저 제공하고 수동 변경 기능을 뒤에 숨겨 놓는 것 자체는 디자인상 필요했으며 바람직한 조치이다. 또한 변경 기능의 접근성을 떨어뜨린 대신, 시차를 고려해 최대 2개의 custom 시계를 추가로 표시할 수 있게 한 건 예전 버전에는 없었던 편리한 기능이다.

아무튼, 어떤 형태로든 사용자로부터 날짜와 시각을 입력받는 UI가 필요하니, 태초에 Windows 95의 날짜· 시각 제어판 애플릿은 이런 형태였다.

사용자 삽입 이미지

여기서 중요한 사실은, Windows 95 시절부터 제어판의 날짜· 시각 대화상자에는 커스텀 컨트롤이 은근히 많았다는 점이다.
이 대화상자는 IE 4 내지 공용 컨트롤 4.7이 등장하기 전부터 존재해 왔다. 그렇기 때문에 저 달력은 SysMonthCal32 공용 컨트롤이 아니다. 공용 컨트롤은 컨트롤 자체에 달력뿐만이 아니라 년과 월을 선택하는 기능이 존재하는 반면, 저 커스텀 컨트롤은 년 월 선택은 위의 콤보 박스에서 따로 하고 있다.
아날로그 시계는 두 말할 나위도 없고, 그 아래의 시간 선택 컨트롤도.. 에디트 컨트롤 몇 개를 내부적으로 갖다 붙인 커스텀 컨트롤이지 SysDateTimePick32가 아니다.

Windows Vista부터는 제어판의 날짜· 시각 대화상자가 구닥다리 자체 구현 달력이 아니라 공용 컨트롤을 사용하는 것으로 구조가 바뀌었다. 하지만 시각을 입력받는 에디트 컨트롤은 공용 컨트롤이 아니라 여전히 재래식 방식이다.
또한, 일부 Windows 버전에는 시간대를 선택하는 UI에 세계 지도 그림이 곁들어진 것이 있는데, 이것도 커스텀 컨트롤로 구현되어 있다.

사용자 삽입 이미지

세계 지도는 내력이 좀 복잡하다. Windows 95에 있었다가 98/ME에서는 잠시 빠졌다. NT 계열인 2000/XP에서는 줄곧 있었지만 Vista부터는 도로 제외된 채 오늘날에 이르고 있다.

Windows 95가 처음 개발되던 당시에는 콤보 상자뿐만 아니라 사용자가 지도의 지역을 클릭하면 그 지역의 시간대가 자동으로 선택되는 기능까지 있었다. UN에서 발행한 지도 자료를 토대로 국가 영역을 다 입력해 넣었으나..
영토 분쟁을 겪고 있는 모 국가들로부터 국경 인식을 왜 그 따위로 하느냐는 항의 메일이 빗발쳤다고 한다. 그 나라에서 제시하는 경계를 다른 쪽 나라에서는 당연히 인정하지 않았고.. 이 기능이 그대로 들어갈 경우 국가적으로 Windows 95를 보이콧 하겠다고 서로 난리도 아니었다. 우리나라의 확장완성형 논란은 이에 비하면 약과로 보일 정도로 자세가 강경했다.

역시 세상에서 무서운 건 정치이다. 고민 끝에 마소에서는 결국 이 기능 자체를 빼 버리고 오로지 콤보 박스로만 시간대를 선택할 수 있게 했다고 한다. 이 일화는 오래 전에 the Old New Thing 블로그에서 운영자가 증언한 내용이다. 세계구급 소프트웨어를 개발하는 데 관여한 사람만이 경험할 수 있는 일이니 참 흥미롭다.

* 여담: 과거 Windows의 동작 방식

믿어지지 않는 사실인데..
과거 Windows 95/98은 제어판의 '날짜/시간'에 들어가서(저 설정 대화상자를 연 뒤) 달력 컨트롤에서 임의의 날짜를 찍거나 연도· 월을 변경하면..
그것만으로도 시스템의 날짜가 즉시 그 날짜로 바뀌었다고 한다! 본인이 가상 머신으로 테스트 해 보니 진짜로 그렇다.

사용자들은 시스템의 날짜를 변경할 의도가 없이, 지금으로부터 가까운 과거나 미래의 달력을 잠시 조회만 할 목적으로 그 대화상자를 종종 꺼내곤 했다. 그도 그럴 것이 작업 표시줄의 우측 하단에 있는 시계만 클릭하면 되기 때문이다. 그런데 달력 컨트롤의 날짜를 클릭하는 것만으로 시스템의 날짜까지 덩달아 바뀌었다니..

물론 대화상자를 ESC/'취소'를 눌러서 닫으면 변경 전의 원래 날짜로 원상복구 되긴 했다. 그러나 일시적으로 날짜가 과거나 미래로 바뀐 동안에는 시스템 날짜에 의존해서 동작하는(업데이트 체크, 알람 등..) 프로그램이 심각한 오동작을 일으킬 위험이 다분히 있었다.

내 경험상, 애플 macOS 진영은 저장이나 인쇄 같은 동작이 수반되는 기능 말고, 단순 설정 기능들은 '취소 cancel/undo'가 딱히 갖춰져 있지 않고 모든 설정들이 변경 즉시 바로 적용되는 편이다.
그러나 마소는 소프트웨어의 UI 디자인 원칙 차원에서 원상복귀/"빠꾸"에 무한 관대할 것을 요구한다. 사용자가 OK라고 최종 승인을 하지 않는 한 말이다.

Windows에서 설정을 변경하는 것만으로 변화를 즉시 경험할 수 있는(preview) 기능은 스피커의 볼륨, 키보드와 마우스의 반응 속도처럼 사용자와 직접 소통하는 컴퓨터의 입출력 장치와 관련이 있는 옵션 정도밖에 못 봤다. 그리고 이마저도 당연히 '취소' 가능하다. 하지만 시스템 날짜도 그렇게 직접 실시간으로 바뀌었다는 건.. 음~ UI를 왜 그렇게 만들었는지 의문이 들게 된다.

그리고 더 이상한 것은 '날짜'(date)만 그렇게 즉시 반응했다는 것이다. '시각'(time)은 사용자가 새로운 시· 분· 초 값을 입력해도 즉시 바뀌지 않았다. 프로그램을 왜 그렇게 만들었는지도 개인적으로 이해되지 않는다.

이런 동작 방식은 Windows 2000/ME에 와서야 개선되어서 달력을 막 건드리더라도 '확인'이나 '적용'을 누르기 전에는 날짜가 절대로 바뀌지 않게 되었다. 날짜· 시각 변경은 DPI 변경과 더불어 20여 년 사이에 Windows에서 위상이 드라마틱하게 달라진 기능에 속한다.

* 여담 하나 더: 컴퓨터 내부에서 날짜와 시각을 표현하는 방식

일상생활에서는 날짜와 시각이 모두 쓰이거나 둘 중 하나만 사용될 때가 있다.

  • 날짜와 시각 모두: 특정 날짜와 시각에 발생하는 알람이나 일회성 약속을 나타낼 때
  • 날짜만: 구체적인 시각이 중요하지 않은 역사 사건을 나타낼 때 (1994년 9월 1일, 분당선 개통)
  • 시각만: 부산 오전 8시 정각, 대전 10시 38분, 서울 12시 10분처럼 날짜가 중요하지 않은 정규 스케줄을 나타낼 때

하지만 컴퓨터 프로그램들은 이들을 모두 동일한 방식으로 저장하고 표현만 필요에 따라 다르게 한다. 무슨 말이냐 하면, 1이 하루를 나타내는 날짜 전용 수 체계와 1이 1초를 나타내는 시간 전용 수 체계를 따로 운용하는 게 아니라, 둘 다 동일한 수 체계를 사용한다는 뜻이다. 기준 연도로부터 얼마의 시간이 경과했는지가 기준이며, 수 체계는 과거에는 32비트 정수가 주로 쓰였지만 요즘은 응당 범위가 훨씬 더 넓은 64비트가 대세이다.

Windows에는 FILETIME이라는 유명한 자료형이 있다. 이것은 1601년 1월 1일 자정으로부터 경과한 시간을 나타내며, 1은 100나노초, 즉 0.1 마이크로초 단위이다. 왜 하필 1601년을 선택했는지는 본인은 모르겠다.
그 반면 Java 언어는 내부적으로 시각을 유닉스 원년인 1970년 1월 1일 자정으로부터 경과한 시간으로 나타내며, 1은 마이크로초를 나타낸다. C 언어에 있는 time() 함수는 기준이 1970년 1월 1일이긴 한데 그냥 초 단위라는 차이가 있다. 이렇듯 시간을 나타내는 방식도 언어와 플랫폼 사이에 미세하게 차이가 있다.

유닉스 원년은 지질학 원년인 1950년과는 딱 20년이라는 차이가 존재한다는 게 흥미롭다. 쉽게 말해, 지질학에서 뭐 "지금으로부터 6500만 년 전에 공룡 멸망", "몇억 몇천만 년 전쯤에 중생대" 이러는 것은 1950년보다 그만치 전이라는 뜻이다. 지질학에서는 방사성 원소 연대 측정법이 본격적으로 쓰인 때를 원년으로 삼았고, 컴퓨터 업계에서는 유닉스 운영체제와 C 언어가 막 개발된 그 시기를 원년으로 삼은 듯하다.

한편, 이런 날짜· 시각과는 달리, 전화번호는 숫자처럼 보이지만 그 형태 그대로 문자열로 저장된다는 점에서 날짜· 시각과는 성격이 다르다. 얘는 아무래도 대소 비교를 하거나 차이를 계산하는 대상은 아니니 말이다. 이런 건 데이터베이스나 엑셀 같은 프로그램으로 주소록 같은 것만 만들어 봐도 바로 공감할 기본적인 내용이다.

Posted by 사무엘

2015/11/13 19:34 2015/11/13 19:34
,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1159

사람이 일생상활에서 취급하는 정보 중에는 일반적인 숫자나 문자열만치 자주 쓰이지는 않지만 날짜· 시각도 있다. 얘들은 컴퓨터 내부에서는 커다란 정수 하나로만 달랑 표현되겠지만 사람에게 보일 때는 일정한 형식을 갖춘 부분숫자의 나열로 표현된다. 또한, 그걸 표현하는 형식 내지 부분숫자들(년월일, 시분초)을 나열하는 순서는 언어나 문화에 따라 대동소이한 차이가 있다.

날짜 문자열은 마치 프로그래밍 언어 문법처럼 형식이 있으며, 문법과 의미 차원에서 정오(맞고 틀림)가 존재한다. 가령, 2015*5#20은 구분자 문자열이 잘못 지정되어서 문법에 어긋나며, 2015/5/100은 문법엔 맞지만 5월 100일이라는 게 존재하지 않기 때문에 의미가 잘못됐다.

날짜· 시각은 그냥 이런 문자열로 입력받게 할 수도 있다. 하지만 필요하다면 달력 같은 걸 내려서 사용자가 전체 날짜를 보면서 내가 원하는 날짜를 마우스로 콕 찍을 수도 있게 하는 게 좋을 것이다. 이 날짜가 무슨 요일인지, 그리고 지금으로부터 과거나 미래로 얼마나 떨어져 있는지를 달력을 통해 시각적으로 확인 가능하다면 날짜를 정확하게 선택하는 데 큰 도움이 되니까 말이다.

그러니 날짜· 시각을 입력받는 GUI 컨트롤은 에디트와 리스트를 겸하여 뭔가 콤보 박스 같은 형태로 만드는 게 좋다.
흔히 date picker라고 불리는데, 이런 컨트롤은 당장 웹사이트에서 더 많이 보는 것 같다. 무슨 예약· 예매 기능이 있는 사이트들이 대표적인 예이다.

대충 만든 사이트라면 년월일을 그냥 각각 콤보 박스에다가 집어넣어 놓곤 한다. 뭐, 생년월일이나 신용카드 유효 기간처럼 수 년 이후의 미래나 수십 년 이전의 과거를 입력받는 거라면 달력을 일일이 스크롤하는 게 번거로우며 단도직입적으로 날짜를 고르는 방식이 나쁘지 않다. 게다가 이런 날짜는 내 의지와는 상관없이 답정너 형태로 존재하는 날짜들이다.
그 반면, 현재로부터 비교적 가까운 날짜에 있을 스케줄을 '내 자유의지'에 따라 잡는 상황에서는 달력이 있는 게 좋을 것이다.

자바스크립트로 만들어진 웹 컴포넌트 말고, Windows의 로컬 프로그램에서 사용 가능한 공용 컨트롤은 95때부터 바로 있었던 게 아니다. 나중에 Internet Explorer 4와 함께 도입되었다(공용 컨트롤 버전 4.7). 바로, 날짜/시각 선택 컨트롤과 달력 컨트롤이며, 클래스 이름은 각각 SysDateTimePick32와 SysMonthCal32이다.

사용자 삽입 이미지

날짜/시각 선택 컨트롤은 스타일을 뭘로 주느냐에 따라서 날짜와 시각 중 하나를 입력받게 할 수 있다. 시각 모드일 때는 컨트롤의 오른쪽에 up/down 버튼이 붙지만, 날짜 모드일 때는 오른쪽에 콤보 버튼이 붙는다. 각 숫자들은 숫자 키로 직접 입력하거나 상하 화살표로 증가· 감소를 시킬 수 있다.

날짜 모드일 때 콤보 버튼을 누르거나 F4 키를 누르면 달력이 나타나서 달력의 날짜를 클릭하는 방식으로 날짜를 지정할 수도 있다. 그리고 달력 컨트롤은 날짜 선택 컨트롤이 일시적으로 표시해 주는 그 달력을 상시 표시해 놓는다.
하긴, 본인 역시 <날개셋> 한글 입력기에서 낱자 선택 콤보 박스를 이런 식으로 만들곤 했다. F4를 눌렀을 때 나타나는 셀 리스트는 drop list일 뿐만 아니라 그걸 단독으로 list box 형태로 쓸 수도 있게 말이다.

그런데 달력 컨트롤은 공용 컨트롤 6.0 것을 쓸 때와 그렇지 않은 재래식 버전을 쓸 때 동작이 살짝 다르다. 재래식은 년과 월을 클릭하면 그냥 년 또는 월을 선택하는 단순한 콤보 박스가 나타나는 반면, 새것은 그걸 클릭하면 달력 자체가 일(day) 대신 월, 1년, 10년 단위로 스케일이 커진다. 마치 지도를 확대/축소하듯이 말이다. 이 새로운 동작 방식은 Windows Vista에서 처음으로 추가되었다.

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

실행 파일의 호환성 옵션으로 들어가서 시각 테마를 끄더라도 공용 컨트롤 6.0의 달력 컨트롤은 그렇게 동작한다. 즉, 이것은 외형의 차이가 아닌 코드 로직의 차이이다.

그러니 날짜/시각 선택 컨트롤과 달력 컨트롤은 ComboBox와 ComboLBox의 관계와 같으며, 리스트 컨트롤과 헤더 컨트롤의 관계와도 얼추 비슷하다.
얘는 공용 컨트롤 중에서도 조금 나중에 도입된 놈이기 때문에 InitCommonControls만 호출해 준다고 해서 초기화가 되지 않는다. 반드시 Ex 버전을 써서 이렇게 초기화해야 한다.

INITCOMMONCONTROLSEX ics;
ics.dwSize=sizeof(ics); ics.dwICC=ICC_DATE_CLASSES;
::InitCommonControlsEx(&ics);

물론, 공용 컨트롤 6.0 매니페스트가 지정된 프로그램들은 저 함수를 호출하지 않아도 모든 공용 컨트롤들을 곧장 사용할 수 있다.
그럼 다음 시간에는 운영체제의 날짜· 시각 설정 UI를 이 컨트롤과 연계해서 살펴보도록 하겠다.

* 여담: 언어적인 이슈

number가 '수'도 되고 '번호'도 되는 것만큼이나, time은 시각도 되고 시간도 돼서 개념이 언어적으로 무척 혼란스럽다. 두 시각의 차가 시간이니 날짜에 대응하는 current time은 '현재 시각'이 돼야 맞다.
'째'(얼추 nth)와 '번째'(nth time)도 이와 비슷한 맥락에서 쓰임이 무척 혼동스럽다. 한국어에서 '째'는 서열이나 순위 개념이고 '번째'는 횟수 정도에 대응한다.
x시 y분에 A역을 발차해서 z시 w분에 B역을 도착한다는 식으로 열차의 운행 스케줄을 적어 놓은 도표도 시간표가 아니라 시각표라고 한다.

Posted by 사무엘

2015/11/11 08:33 2015/11/11 08:33
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1158

과거 Windows XP 시절부터 눈치 챈 분이 계시려나 모르겠다.
XP에서부터 아시다시피 대화상자 컨트롤들에 테마가 적용되어서 얘들의 비주얼이 알록달록 한결 예뻐졌다. 단, 모든 프로그램에 테마가 자동 적용되는 건 아니며 공용 컨트롤 6.0을 사용한다는 표식이 존재하는 프로그램에 대해서만 테마가 적용된다. 이것은 Windows 프로그래머라면 추가적으로 알고 있을 것이다.

그런데, 콤보 박스의 경우 공용 컨트롤 6.0 것을 사용하면 동작이 약간 달라졌다. ▼ 버튼을 눌러서 drop list를 꺼내면 drop list의 크기가 예전보다 훨씬 더 길쭉해져 있다. 프로그램이 리소스 차원에서 지정해 준 것보다 훨씬 더 길어졌다.
이것은 테마의 적용 여부와는 무관하게 공용 컨트롤 6을 적용하는 것만으로도 나타나는 변화였다.

다음은 <날개셋> 편집기의 문자표 대화상자를 Windows 2000에서 구동한 것과 XP(및 그 이후 모두 포함)에서 구동한 것의 차이를 나타낸 스크린샷이다. 테마나 글꼴과는 무관하다는 것을 보이기 위해 XP에서도 고전 테마를 사용했고, 글꼴 역시 Tahoma로 통일시켰다.
문자표뿐만이 아니라 “보기-편집화면 설정”에서 이미 30개가 넘는 글꼴이 존재하는 한글 글꼴 콤보 박스를 내려 봐도 같은 현상을 볼 수 있다.

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

그리고 이것은 비단 내 프로그램에서만 발생하는 차이가 아니다. 다음은 Windows 2000과 XP의 워드패드에서 글꼴 콤보를 내린 모습이다. 워드패드는 Windows 7에서 크게 바뀌었지 2000과 XP는 외형의 차이가 거의 없으니 말이다.

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

콤보 박스에서 drop list의 크기는 원래 콤보 박스가 생성될 때 CREATESTRUCT에 지정되어 있던 크기로 결정되어 왔다. 그 크기는 따로 저장된 뒤, 지금 당장 콤보 박스는 아이템을 1줄만 표시 가능한 정도로 세로 크기가 자동으로 조정되었다.

하지만 공용 컨트롤 6에 있는 콤보 박스는 알 수 없는 이유로 인해 이런 전통적인 동작이 바뀌었다. 원래 지정되었던 세로 크기는 무시되고 drop list는 아이템을 최대 30개까지 표시하는 크기로 지정된다. 그래서 XP에서부터 공용 컨트롤 6을 사용하는 프로그램은 콤보 박스의 drop list가 굉장히 커진 것이다.
그 대신 이 개수를 얻어 오고 바꾸는 메시지가 새로 추가되었다. CB_(GET/SET)MINVISIBLE이 그것이다. 이 메시지는 공용 컨트롤 6 기반의 콤보 박스에서만 사용 가능하다. 개수의 기본 초기값이 30인데, 이것은 이례적으로 상당히 큰 값이다.

단, 공용 컨트롤 6이라 해도 CBS_NOINTEGRALHEIGHT 스타일이 지정된 콤보 박스에는 이런 새로운 정책이 전혀 적용되지 않고 이전의 콤보 박스와 동일한 크기로 drop list가 튀어나온다. 이 스타일은 크기 보정을 전혀 하지 않고 마지막 줄에 아이템의 일부가 잘려 나오는 것도 감수한다는 옵션인데.. 개인적으로 이런 옵션이 왜 있는지 모르겠다. 이걸 사용하는 콤보 박스는 난 지금까지 전혀 못 봤다. 마치 extended UI만큼이나 실용적인 의미가 거의 없음.

여러모로 Windows GUI에서 콤보 박스의 동작과 메시지 API가 왜 이렇게 설계되었는지 본인으로서는 이해할 수 없다. (1) 공용 컨트롤 6이 됐다고 해서 저런 동작이 함부로 바뀌어서는 안 되며, (2) 콤보 박스의 drop list 크기는 자신의 원래 크기를 기반으로 동작해야 한다. 그리고 (3) drop list의 크기를 알아 오거나 바꾸는 메시지는 진작부터 있어야 했다.
XP에서부터 콤보 박스의 drop list 크기가 갑자기 커졌다는 건 오래 전부터 알고 있었지만 왜 그런지는 10년 가까이 제대로 모르고 있었다. 이유를 알게 됐다고 해서 그 이유를 수긍하는 것도 아니긴 하지만.

이것 말고도, Windows XP 첫 버전의 공용 컨트롤 6에서는 owner draw 방식이어서 문자열이 필요하지 않은 콤보 박스에다가도 아이템을 추가할 때 그냥 NULL을 주면 프로그램이 뻗는 어이없는 버그가 있었다. 원래는 그렇게 안 해도 돼야 되는데. 그래서 꼭 0-length 문자열 ""이라도 집어넣어 줘야 했다. 테마 내지 공용 컨트롤 6 기반이 아닌 기존 컨트롤에서는 문제가 없었으니 명백한 운영체제의 버그였다.

<날개셋> 한글 입력기가 최초로 XP 테마를 도입한 버전이 2.3이었는데, 바로 이 버그 때문에 엄청 곤혹을 치렀었다. 이 버그는 XP sp1에서 바로 고쳐지긴 했지만, 비주얼 말고 도대체 뭘 고쳤길래 같은 콤보 박스에 이런 동작이 차이가 났는지 본인으로서는 알 길이 없다. 요컨대 공용 컨트롤 6의 컨트롤들은 설령 다른 테마 없이 고전 테마로 표시된다 하더라도 근간이 기존 컨트롤과는 근본 출신이 다르다.

Posted by 사무엘

2015/11/02 08:33 2015/11/02 08:33
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1155

※ 메모리 단편화

컴퓨터에서 무작위 읽기/쓰기가 가능한 모든 기억장치.. 즉 RAM, 파일 시스템, 데이터베이스 따위에는 모두 구조적으로 단편화라는 문제가 존재한다.
메모리를 10바이트씩 찔끔찔끔 요청했는데 최소 할당 단위 제약 때문에 실제로는 수백 바이트 단위로 성큼성큼 용량이 짤려 나간다거나(내부 단편화),
전체 남은 용량은 1000바이트인데 한 600바이트 정도 연속된 구간이 없어서 메모리 할당이 실패하는 외부 단편화가 모두 존재한다.

메모리라는 게 1차원적인 공간이기 때문에 이건 뭐 어쩔 수 없다.
그래서 컨텐츠가 실제로 차지하는 용량보다 전체 소모 용량이 더 커지게 되고, 이런 걸 관리하는 프로그램이나 유틸리티에는 조각 모음(defrag), shrink, compact 같은 동작을 강제로 수행하는 기능이 있다. (뭐, 디스크 중에서 SSD는 예외적으로 조각 모음이 필요하지 않은 구조라고는 하지만.)

디스크는 애초부터 파일 시스템의 지배 하에 있으며 그 시스템이 제공하는 방식대로 디렉터리와 파일 이름을 통해서만 내용에 접근 가능하다. 일반적인 응용 프로그램이 디스크를 무슨 실린더 번호 x, 트랙 y, 섹터 z 같은 형태로 무식하게 접근하는 경우는 거의 없다. 그런 방식은 오늘날의 운영체제에서는 더욱 금기시되고 있다.

그렇게 파일명이라는 고수준 추상 계층이 있는 덕분에 디스크는 내부적으로 막 조각 모음을 해도 딱히 파일을 못 찾는 일이 발생하지는 않는다. 저수준 처리는 운영체제의 파일 시스템이 알아서 다 처리해 준다. 또한 디스크 정도면 물리적으로 액세스를 하는 데서 발생하는 병목이 소프트웨어적인 추상화 계층을 거치는 시간보다 훨씬 더 길기도 하고 말이다. 사용자에게는 외부 단편화보다는 클러스터 최소 단위로 인한 내부 단편화가 현실적으로 더 와 닿는다.

그런데 RAM은 디스크와는 사정이 다르다. 단편화를 예방한답시고 함부로 컨텐츠들을 재배치하면 memcpy 오버헤드는 둘째치고라도 그 메모리 주소를 직접 가리키고 있던 수많은 포인터들이 작살이 나 버린다.
메모리 자원이 극도로 가난하고 열악하던 16비트 Windows 시절에는 운영체제의 global/local heap으로부터 메모리를 할당받고 나면 곧바로 포인터가 돌아오는 게 아니라 핸들 하나만이 돌아왔다. 이 핸들이 가리키는 메모리는 운영체제의 사정에 따라 수시로 재배치될 수 있는데, 메모리를 실제로 사용할 때만 lock을 걸어서 위치를 고정시킨 뒤, 포인터를 얻어와서 메모리를 참조하곤 했다. 사용이 끝나면 다시 unlock을 해 줘야 한다.

이것이 바로 GlobalAlloc - GlobalLock - GlobalUnlock - GlobalFree 사이클이다. 재배치를 하는 이유는 당연히 메모리 단편화를 극복하고, 연속된 긴 메모리 공간을 언제나 확보하기 위해서이다. 16비트 시절에 메모리 블록이나 리소스 같은 데에 discardable, resident, non-resident 같은 속성이 달려 있던 이유는, 수시로 재배치 내지 재로딩 같은 빡센 메모리 관리에 대응하기 위해서이다.
운영체제가 자동으로 무슨 garbage collect를 해 주는 것도 아니고, 저런 일을 해야만 했다는 게 참 안습하다.

여기서 우리가 알 수 있는 점은, 32비트 정도 되는 주소 공간에서 가상 메모리가 제공되는 게 프로그래머의 관점에서 얼마나 축복이냐 하는 것이다. 4기가바이트 정도 넉넉한 공간이 있으면, 단편화 문제는 주소빨로 어느 정도, 상당 부분 극복이 가능해진다. 어지간히 단편화가 심한 상태라 해도, 또 대용량 메모리 요청이 들어오면 걍 다음 주소를 끌어다가 물리 메모리에다 대응시켜 쓰면 되기 때문이다.

그 연속된 가상 메모리 주소를 실제로는 여기저기 흩어졌을 가능성이 높은 지저분한 물리 메모리로 대응시키는 건 운영체제와 CPU의 몫이다. 물리 메모리가 부족하면 하드디스크 스와핑까지 알아서 해 준다. 가상 메모리 덕분에 프로세스간에 보안이 더 향상된 것도 덤이고 말이다.

이것이 RAM과 디스크의 차이이다. 디스크에 파일명이 있다면 RAM에는 가상 메모리 메커니즘이 있다. 한 주소 공간 안에서 스레드가 여러 개 있는 경우 가상 메모리의 필요성은 더욱 커진다.
물론, 세상에 공짜는 없으니, 가상 메모리는 메모리를 관리하기 위한 추가적인 메모리도 적지 않게 소요하는 테크닉인 걸 알아야 한다. 물리적인 메모리뿐만이 아니라 가상 메모리 주소 영역 자체도 떼먹는다.
오늘날 64비트 운영체제라 해도 어마어마하게 방대한 공간인 64비트 전체를 사용하는 게 아니라 40비트대 정도만 사용하는 것도 이런 이유 때문이다.

※ 옛날 이야기

옛날의 프로그래밍 언어나 소프트웨어 플랫폼을 살펴보면, 메모리와 관련하여 오늘날 당연한 기본 필수라고 여겨지는 요소가 대놓고 빠진 것들이 적지 않아 놀라게 된다.

(1) 예를 들어 옛날에 포트란 언어는 함수 호출은 가능하지만 초기에는 동일 함수에 대한 중첩/재귀 호출이 가능하지 않았다. 세상에 뭐 이런 언어가 다 있나 싶다..;; 함수 안에서 지역 변수의 사용이 스택 기반으로 되어 있지 않고 늘 고정된 주소로만 접근하게 돼 있어서 그랬던 모양이다.

오늘날의 프로그래밍 언어에서야 지역 변수는 스택의 기준 주소로부터 상대적인 위치를 건드리게.. 일종의 position independent code 형태로 접근된다. 재귀 호출 지원뿐만 아니라 코드 실행 주체가 증가하는 멀티스레드 환경에서는 각 스레드가 또 독립된 스택을 갖고 있으니 절대 고정 주소가 더욱 의미를 상실하기 때문이다. 멀티스레드는 thread-local이라는 일종의 새로운 scope까지 만들었다.

(2) 한편, 프로그래밍 언어 쪽은 아니지만, Win32의 구현체 중에 제일 허접하고 불안정하고 열악하던 Win32s는..
멀티스레드도 없고 각 프로세스마다 독립된 주소 공간이 없는 건 그렇다 치는데... DLL은 자신이 붙는 각 프로세스별로 자기만의 독립된 데이터 공간마저도 보장받지 못했다. 16비트 DLL과 다를 바가 없다는 뜻.

옛날에 아래아한글 3.0b는 윈도 95나 NT 말고 3.1 + Win32s에서 돌아갈 때는 무슨 자기네 고유한 메모리 서버 프로그램을 먼저 실행한 뒤에야 실행 가능했다. 이제 와서 다시 생각해 보니, 그 메모리 서버가 하는 일이 바로 DLL별로 고유한 기억장소를 할당하는 것과 관련이 있지 않았나 싶다. 아래아한글의 소스를 모르는 상태에서 그냥 개인적으로 하는 추측이다.

아시다시피 16비트 Windows는 가상 메모리 같은 게 없다 보니, 콜백 함수의 실행 context를 레지스터에다 써 주는 것조차 소프트웨어가 수동으로 해야 할 정도로 진짜 가관이 따로 없었다.

※ 쓰레기(다 쓴 메모리) 수집

끝으로 garbage collector 얘기다.
heap으로부터 할당하는 메모리는 너무 dynamic한지라 언제 얼마만치 할당해서 언제 해제되는지에 대한 기약이 없다. 그걸 소스 코드만 들여다보고서 정적 분석으로 완벽하게 예측하는 건 원천적으로 불가능하다.

하지만 정해진 scope이 없는 동적 메모리를 잘못 건드려서 발생하는 소프트웨어 버그는 마치 자동차의 교통사고처럼 업계에서 상당히 심각한 문제이다.
memory leak은 당장 뻑이 나지는 않지만 프레임 단위 리얼타임으로, 혹은 수 개월~수 년간 지속적으로 돌아가는 소프트웨어에서는 치명적이다. 또한 다른 메모리/포인터 버그도 단순히 혼자만 뻑나는 걸로 끝나면 차라리 다행이지, 아예 악성 코드를 실행시키는 보안 문제로까지 상황을 악화시킬 수 있다.

이 동적 메모리 관리를 사람에게 수동으로 맡겨서는 안전하지 못하니, 메모리 자원 회수를 프로그래밍 언어 런타임 차원에서 자동으로 보장되게 하는 기법이 연구되어 왔다.
고전적인 reference counting 테크닉은 C++의 생성자/소멸자 패러다임과 맞물려서 일찍부터 연구되어 왔으며 smart pointer 같은 구현체도 있다.

이건 원리가 아주 간단하며, 언어 차원에서 포인터의 scope가 벗어나는 족족 메모리가 칼같이 회수되는 게 컴파일 시점에서 보장된다. 그래서 깔끔한 것 하나는 좋다.
허나 이 기법은 생각보다 비효율과 단점도 많다. 대표적인 논리적 결함인 순환 참조는.. 서로 다른 두 객체가 상대방을 서로 참조하여 똑같이 참조 횟수가 1보다 커지고, 따라서 둘이 메모리가 결코 해제되지 않아서 leak으로 남는 문제이다.

즉, 레퍼런스 카운팅이 잘 동작하려면, 참조를 받은 피참조자는 자신을 참조하는 놈을 역참조하지 말아야 한다. 이걸 어기면 객체간의 레퍼런스 카운트가 꼬여 버린다.
문제는 이걸 일일이 조심하면서 코드를 작성하는 게 상황에 따라서는 차라리 걍 메모리 자체를 수동으로 관리하는 게 나을 정도로 효율이 떨어질 수 있다는 것이다. 게다가 고리가 어디 A-B, B-A 사이에만 생기겠는가? A-B, B-C, C-A 같은 식으로 더 골치 아프게 생길 수도 있다. 참조 관계는 정말로 cycle이 없이 tree 형태로만 가야 한다.

그러니 이 문제는 예상 외로 굉장히 심각한 문제이다. 멀티스레드에서의 '데드락'하고 다를 바가 없다! 서로 뭔가 꼬여서 끝이 안 난다는 점, 잡아 내기가 극도로 어렵다는 점이 공통점이다.
성능을 더 희생하고라도 메모리 leak 문제를 완전히 다른 방식으로 접근한 전용 garbage collector가 괜히 등장한 게 아니었겠다 싶다.

가비지 컬렉터라고 해서 무슨 용 빼는 재주가 있는 건 아니다. 기본적으로는 당장 접근 가능한 메모리로부터 출발해서 그 메모리로부터 추가로 접근 가능한 메모리 블록을 줄줄이 순회하여 표시를 한 뒤, 표시가 없는 메모리를 죄다 해제한다는 아이디어를 기반으로 동작한다. 동적으로 할당받은 메모리 내부에 또 동적 할당 메모리의 포인터가 있고, 그게 또 이상하게 얽히고 배배 꼬인 걸 어떻게 일일이 다 추적하는지 더 구체적인 방법은 잘 모르겠지만.

어찌 보면 단순무식하다. 주인 없이 주차장에 장기간 방치되어 있는 폐자전거들을 일괄 처분하기 위해 모든 자전거에 리본을 달아 놓은 뒤, 일정 날짜가 지나도록 리본이 제거되지 않은 자전거를 갖다 버리는 것과 개념적으로 비슷하다! 혹은 기숙사의 공용 냉장고에서 주인에게로 접근(?)이 안 되는 장기 방치 식품을 주기적으로 제거하는 것과도 비슷한 맥락이다. 단지 좀 더 성능을 올리기 위해, 메모리 블록들을 생존 주기별로 분류를 해서 짬이 덜 찬 메모리가 금방 또 해제될 가능성이 높으므로 거기부터 살펴보는 식의 관리만 하는 정도이다. 자바, .NET의 가상 머신들도 이런 정책을 사용한다.

이건 즉각 즉각 자원이 회수되는 게 아니며, 리얼타임 시스템에서는 적용을 재고해야 할 정도로 시공간 오버헤드도 크다. 그러나 한번 수집이 벌어질 때 랙이 있다는 말이지, 매 대입 때마다 시도 때도 없이 카운터 값을 변화시키고 그때 스레드 동기화까지 해야 하는 레퍼런스 카운팅도 성능면의 약점은 상황에 따라 피장파장일 수 있다.

언어 차원에서 이런 가비지 컬렉터가 제공되어서 delete 연산자와 소멸자 자체가 존재하지 않는 언어가 요즘 추세이다. 자바나 C#처럼. 하지만 메모리는 그렇게 자동으로 수집되지만, 파일이나 다른 리소스 핸들은 여전히 수동으로 해제를 해야 할 텐데 무작정 소멸자가 없어도 괜찮은지는 잘 모르겠다. 본인은 그런 언어로 대규모 프로그램을 작성한 경험이 없다. C++ 이외의 언어에서는 RAII 개념이 아예 존재하지 않는 건지?

Posted by 사무엘

2015/09/20 08:28 2015/09/20 08:28
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1140

Windows XP에서부터 내가 생성하는 exe 바이너리의 내부에 xml 문서 리소스 형태의 메타데이터를 집어넣는 게 필수 관행이 됐다.
가장 대표적인 용도는 (1) 내가 사용하는 시스템 DLL의 버전을 지정해서 로딩 방식을 시스템 디렉터리 말고 딴 곳으로 강제로 바꾸는 것이다.

이 방식으로 (1-1) 공용 컨트롤을 6.0 버전을 사용한다고 명시해야 각종 컨트롤에 XP의 새끈한 테마가 적용되어 나왔다. 테마 기능을 추가하는 과정에서 comctl32.dll이 XP 이전과 XP 이후 것은 하위 호환성이 없이 서로 단절됐기 때문이라 한다.
내 프로그램의 비주얼이 시대에 뒤떨어진 구닥다리처럼 보이지 않게 하려면 이거 지정은 선택의 여지가 없는 필수이다.

XP 이후로 지금까지 comctl32.dll이 7.0으로 올라간다든가 해서 또 breaking changing을 겪지는 않았다.
Windows 8부터는 명목상 고전 테마와 표준 테마의 구분이 없어지긴 했지만, 그래도 공용 컨트롤 6.0 지정을 안 해도 되는 건 아니다. 구닥다리 비주얼 자체가 호환성 때문에 완전히 없어진 건 아니기 때문이다.

뭐 comctl32가 가장 대표적이긴 하지만 이 외에 (1-2) GDI+ (gdiplus.dll)와 비주얼 C++의 CRT DLL, 그리고 MFC DLL이 지난 200x년도에 이 방식을 사용해 로딩된 적이 있다.
Windows XP 시절엔 WinSxS 디렉터리에 파일이 몇 개 없었지만 Vista 이후부터는 도대체 뭐가 들어갔는지 여기에 디렉터리 수가 무려 수천~만수천 개에 달할 정도로 많아졌다. 199x년대에 한번 DLL hell을 겪은 뒤, 너무 난장판이 돼 가는 시스템 디렉터리를 이런 식으로 정리를 하려 했던 모양이다. 같은 DLL이라도 버전별로 쫙 따로 분류를...

하지만 DLL을 배포하기가 너무 불편하다는 원성이 빗발치면서 VC++ 201x부터는 CRT와 MFC DLL 로딩 방식이 다시 재래식 시스템 디렉터리 기반으로 복귀했다.
또한 GDI+도 오늘날은 기존 GDI만큼이나 재래식 유물로 전락했고... 요즘은 딱히 이 WinSxS 기반 DLL 로딩이 활발히 쓰이고 있는지 모르겠다.

DLL 버전 설명이 좀 길어졌는데, 이것 말고도 xml에 들어가는 정보로는
(2) 내 프로그램이 고해상도+가변 DPI를 인식한다는 플래그
(3) 반드시 관리자 권한이 필요하다는 식의 권한 플래그

가 Vista에서 추가되었다. 시스템 차원에서의 고해상도 DPI 설정이야 더 말할 것도 없고 Windows 8.1부터는(8도 아님) 실행 중에 on-the-fly로 모니터 단위 DPI 설정이 변경되는 것에도 추가로 대비가 돼 있어야 한다. 그런 표식이 없으면 그냥 그래픽 카드의 힘으로 프로그램 화면이 그대로 기계적으로 확대 축소된다.

먼 옛날, Windows NT 3.x 내지 95 시절엔 화면 해상도를 재부팅 없이 on-the-fly로 바꾸는 것만 해도 굉장한 혁신이었는데 참 격세지감이다. 시스템 DPI도 예전에는 재부팅이 필수였지만 그래도 Windows Vista쯤부터는 그냥 재로그인만 하면 되게 규제가 완화됐다.

그리고 Vista부터인지 7부터인지는 모르겠지만 (4) 이 프로그램이 인식하는 운영체제 목록도 xml에다 명시하게 되었다. PE 실행 파일 포맷에 명시된 OS 버전의 역할을 어느 정도 대신하게 됐다.
Windows 8.1부터는 GetVersionEx가 알 수 없는 이유로 인해 봉인되어서, 프로그램이 인식하는 걸로 등록되지 않은 최신 운영체제의 버전은 Windows 8 이상으로는 알려주지 않게 바뀌었다. 도대체 이런 만행을 왜 무슨 이유 때문에 저질렀는지 난 모르겠지만.. 도대체 버전을 숨겨서 뭐 하게?

그리고 Windows 7도.. 자기 버전이 명시되지 않은 EXE 프로그램이 이름이 setup이라거나 하면, 실행 후에 "이 프로그램은 혹시 설치 프로그램인가요? 뭔가가 제대로 설치/제거되지 않았을 수 있습니다" 이런 꼬장을 부린다. 무슨 호환성과 관련된 조치인 듯하다.

결국 처음에는 공용 컨트롤 때문에 사용하기 시작한 xml인데 이제는 무슨 실행 프로그램을 제대로 만들려면 메타데이터 xml을 집어넣는 건 거의 필수가 되었다. 이 프로그램이 미래에 운영체제에서 새로 도입되는 시스템이나 기능을 받아들일 준비가 됐다는 것을 나타내는 증서 역할을 하다 보니, 들어가는 내용이 점점 증가하고 있다. 저 네 가지 아이템 말고 내가 또 빠뜨린 게 있는지 모르겠다.
그리고, PE 헤더에 운영체제 버전과 서브시스템 버전은 왜 따로 존재하고 둘의 차이가 무엇인지도 본인은 궁금하다.

* 추가 설명

사실, 메모장이나 워드패드처럼 Windows에 포함되어 있는 기본 프로그램들은 내부의 매니페스트 XML을 열어 보면 공용 컨트롤, 고해상도 DPI, 실행 권한 같은 설정은 있지만 굳이 운영체제 정보가 들어있지는 않다. 특정 버전의 운영체제에 포함되어 있는 프로그램들이 또 그 운영체제 GUID가 내장되어 있다거나 하지는 않다.

이때는 PE 헤더 차원에서 명시된 운영체제 버전이 활용된다. 이 버전이 Windows 8.1에 달할 정도로 높은 값이 들어있으면 GetVersionEx 함수도 Windows 8.1의 버전도 정확하게 되돌려 준다. 다만 이 경우, 그 실행 파일은 Windows 8.1 미만의 운영체제에서는 전혀 실행되지 않는다는 것을 감안해야 한다. 심지어 7에서도 실행이 거부된다.

Posted by 사무엘

2015/09/03 08:30 2015/09/03 08:30
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1134

« Previous : 1 : ... 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : ... 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/03   »
          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:
2620252
Today:
3251
Yesterday:
1544