1. 메뉴 -- 긴 역사를 자랑하는 GUI 구성요소
'메뉴'(menu)라는 단어는 순우리말로는 흔히 차림표라고 하고, 식당의 음식 메뉴 아니면 컴퓨터 소프트웨어의 GUI 요소라는 꽤 이질적인 두 심상이 결합해 있는 독특한 단어이다. 이런 점에서 '메뉴'는 '마우스'하고도 비슷한 구석이 있는 것 같다.
메뉴는 GUI라는 개념이 컴퓨터에 도입된 이래로 굉장히 오랜 시간을 인간과 함께해 왔다. 워낙 중요하고 필수적인 기능이기 때문에 Windows 운영체제는 아예 API 차원에서 창을 하나 만들 때 메뉴 핸들을 같이 넘겨 줄 수 있게 돼 있다. (CreateWindowEx 함수) Windows는 그래도 보급 메뉴(?) 지원을 무시하고 GUI 툴킷이 자체 구현한 싸제 메뉴를 붙일 여지라도 있지만, Mac OS는 메뉴 bar가 무조건 화면 위에 붙박이로 고정이고 게다가 운영체제의 시스템 메뉴와 일심동체로 통합되어 있기 때문에 싸제 메뉴 같은 건 있을 수 없다.
물론, 너무 무난하고 밋밋한 관계로 요즘 만들어지는 응용 프로그램에서는 메뉴가 천덕꾸러기처럼 취급되는 면모가 없지는 않다. 메뉴+툴바가 리본 UI로 대체된 것은 물론이고, 메뉴가 있더라도 메뉴 bar를 평소에는 감춰 버리고 Alt키를 눌러야만 마지못해 보여 준다. 글쎄, 이러다가 나중에 또 복고풍으로 메뉴로 돌아가지는 않을지?
그리고 어떤 경우든 사각형 안에서 선택막대로 기능을 선택하는 전통적인 메뉴 개념 자체가 없어지는 일은 없을 것이다.
난 닷넷 프레임워크는 그냥 운영체제의 보급 메뉴를 자기 고유 API로 감쌌는줄 알았는데, 그렇지 않다는 걸 알게 되어 개인적으로 놀란 적이 있다. 닷넷 기반 GUI 프로그램은 기본적으로 Office XP 스타일을 적당히 따라 한 싸제 메뉴가 나온다.
보급이든 싸제든, 어쨌든 GUI에서 전통적인 메뉴는 F10을 눌렀을 때 화면 상단에 나타나는 가로줄 메뉴, 혹은 main 메뉴를 가리키는 경우가 많다.
그러나 이것 외에 어떤 개체를 마우스로 우클릭했을 때 나타나는 Context 메뉴, 혹은 팝업 메뉴는 좀 더 나중에, 1990년대 중반에 도입되었다. 윈도우 95 이전에 3.x 시절에는 그림판으로 두 색깔을 번갈아가며 쓸 때 말고는 마우스를 우클릭할 일 자체가 거의 없었던 것 같다. 팝업 메뉴를 띄우는 기능 자체는 3.x 시절에도 있었을 텐데도 불구하고 말이다.
2. HMENU
자, 그럼 Windows 플랫폼 프로그래밍의 관점에서 운영체제의 메뉴 개체에 대해서 좀 더 살펴보자.
이 메뉴라는 놈을 관리하는 개체는 바로 HMENU이다. 얘는 메뉴에 표시시킬 각종 아이템들과 그것들의 상태들을 보관하고 있는 일종의 연결 리스트의 포인터라고 생각하면 된다. 어떤 메뉴 항목에는 또 부메뉴가 딸려 있을 수 있으므로 메뉴는 일종의 재귀성까지 갖추고 있다.
메뉴는 잘 알다시피 리소스의 형태로 쉽게 만들어 내장시킬 수도 있다. 그러나 HMENU 값은 아이콘이나 액셀러레이터, 마우스 포인터 같은 여타 리소스들과는 달리, read-only 리소스가 아니다. 이게 무슨 말인지 배경을 좀 설명하자면 이렇다.
16비트 Windows 시절에는 EXE/DLL에 있는 리소스 데이터를 얻기 위해서 별도로 파일을 열고 메모리를 할당하고 고정하는 등의 절차가 필요했다. 그러나 운영체제가 32비트 환경으로 바뀌면서 실행 파일의 로딩 방식이 memory mapping 방식으로 바뀌었기 때문에, 모듈에 내장된 리소스를 찾는 건 그냥 이미 로딩된 메모리의 주소만 되돌리는 형태로 아주 간단해졌다.
그래서 예전과는 달리, 이제는 한번 fetch해 온 리소스 데이터에 대해서 FreeResource 같은 함수를 호출할 필요가 없어졌다. 그 리소스를 제공하는 EXE의 실행이 종료되거나 DLL이 Unload될 때 어차피 자동으로 한꺼번에 해제되기 때문이다.
일반적인 읽기 전용 리소스는 그런 간소화의 혜택을 입게 되었다.
그러나 메뉴의 경우는 모듈에 내장된 메뉴 데이터의 포인터만 얻어 오는 걸로 끝이 아니라, 그 데이터를 토대로 메뉴 연결 리스트를 별도로 재구성한다. 사용자는 그 연결 리스트의 데이터를 변경함으로써 메뉴에 별도의 항목을 추가하거나 삭제하고, 체크 표시나 disable 처리를 할 수 있다.
그렇기 때문에 LoadIcon, LoadCursor 등의 리턴값은 Free를 할 필요가 없지만, LoadMenu 핸들의 리턴값은 반드시 DestroyMenu를 해 줘야 한다. (물론, 아이콘 같은 리소스라 해도 모듈 내장이 아니라 직접 동적으로 생성한 놈이라면 Destroy*함수를 호출해서 수동으로 소멸해야 하는 건 변함없음.)
HMENU는 내부적으로 딱히 reference counting을 하지는 않는 단순한 구조이다.
윈도우와 연결되어 있는 메뉴는 윈도우가 소멸될 때 같이 자동으로 소멸되며(물론 부메뉴들도 재귀적으로 다 같이), 한 메뉴 인스턴스가 여러 윈도우에서 공유되지는 않는다. '이동', '닫기' 같은 명령이 있는 시스템 메뉴가 있는데, 필요하다면 사용자가 이 메뉴 역시customize할 수 있다.
3. API 디자인
(1) Windows API의 설계 관점에서 흥미로운 것은, 정수로 식별하는 ID를 받는 곳에다가 필요에 따라 메뉴 핸들도 같이 집어넣게 한 게 종종 보인다는 점이다.
CreateWindowEx 함수의 경우, HMENU는 생성하려는 윈도우가 팝업 같은 메이저 윈도우이면 메뉴 핸들이고, 메뉴를 갖는 게 의미가 없는 자그마한 마이너 자식 윈도우이면 정수 ID를 의미한다.
물론 메뉴 핸들과 ID가 동시에 쓰일 일은 없는 건 사실이다. 윈도우의 ID는 대화상자의 차일드 컨트롤들을 식별할 때에나 쓰는 것이니 말이다.
하지만 어째 이 둘을 실제로 공유시킬 생각을 했는지 궁금하다. 어지간하면 그냥 내부 구조체에다 별도의 멤버를 따로 둘 법도 한데, Windows 1.x 시절의 헝그리 정신을 살려, 메모리 절약을 위해 공용체를 썼는가 보다.
또한 메뉴 API도 AppendMenu나 InsertMenu를 보면, 일반 메뉴 아이템에 대해서는 명령 ID를 전달하는 항목에, MF_POPUP이 지정된 하위 메뉴 아이템에 대해서는 또 HMENU를 typecast하여 전달하게 되어 있다.
(2) CreateMenu와 CreatePopupMenu 함수를 왜 따로 만들어 놨는지 영 이해가 안 된다. HINSTANCE와 HMODULE만큼이나 사실상 의미 없는 구분이 돼 있다.
응용 프로그램의 main 메뉴나 우클릭 팝업 메뉴는 화면에 보이는 형태만 다를 뿐, 부메뉴를 가질 수 있는 재귀적인 형태인 것도 똑같고 내부 자료 구조가 달라야 할 것은 없다.
하긴, 그러고 보니 HCURSOR도 HICON하고 내부적으론 거의 같은 자료구조라고 하지. (핫스팟 위치만 추가됐을 뿐)
(3) 메뉴의 상태를 나타낼 때 MF_GRAYED와 MF_DISABLED를 따로 만들어 놓은 건 개인적으로 무척 기괴하게 여겨진다.
MF_GRAYED는 우리가 흔히 보는 '사용할 수 없는' 메뉴 아이템이다. 흐리게 표시되고 선택도 되지 않는다. 그러나 MF_DISABLED는 선택만 안 될 뿐 흐린 표시는 아니다.
이건 솔직히 말해서 잉여력이 넘치는 구분이다.
그래서 심지어는 MS 내부의 개발자들조차도 이를 혼동해 있다.
고전 테마를 쓰고 있을 때는 MF_DISABLED를 설정한 메뉴가 '일반 글자'로 표시된다.
그러나 Luna나 Aero 같은 테마가 적용되어 있을 때는 이게 MF_GRAYED와 동일하게 '흐린 글자'로 표시된다! 문서화된 바와도 다르고 일관성 없게 동작한다는 뜻이다. 내 말이 믿어지지 않으면 당장 프로그램을 짜서 확인해 보기 바란다.
일상생활에서는 MF_DISABLED는 전혀 신경 쓸 필요 없고 MF_GRAYED만 쓰면 될 것 같다.
(4) RemoveMenu, DeleteMenu, DestroyMenu의 차이가 뭘까?
먼저 DestroyMenu는 HMENU 자체를 완전히 소멸시키는 함수이다. 메뉴와 부메뉴들이 모두 다 사라지고 해당 핸들은 사용할 수 없게 된다.
RemoveMenu와 DeleteMenu는 메뉴 안에 있는 한 항목을 제거한다. 제거할 항목을 순서 인덱스 또는 명령 ID로 지정할 수 있다. 부메뉴를 가진 항목이나 항목 구분용 separator는 명령 ID를 갖고 있지 않으므로 반드시 순서 인덱스만 지정 가능할 것이다.
둘의 차이는 딱 하나. 부메뉴를 가진 항목을 지울 때 부메뉴 핸들을 재귀적으로 destroy하느냐(Delete) 안 하느냐(Remove)이다. 마치 '프로젝트 목록에서 파일 제거'와, '파일 제거 + 실제로 디스크 상에서도 삭제'의 차이와 비슷한 맥락이다.
(5) 사실, Windows의 메뉴 API가 좀 더 객체지향적으로 설계되었다면, HMENU뿐만 아니라 각각의 메뉴 아이템을 나타내는 HMENUITEM 같은 자료형도 또 만들었을 것이다.
지금은 그렇지 않기 때문에 메뉴 아이템을 식별할 때마다 매번 HMENU와 UINT nID, 그리고 nID가 명령 ID인지, 순서 인덱스인지를 나타내는 플래그를 넘겨줘야 한다. 메뉴 항목을 편집하거나, 어디 뒤에 삽입하거나 삭제하는 함수들이 전부 저 인자들을 일일이 받는다. 내가 보기엔 무척 지저분하다.
또한 동일한 기능을 하는 API가 구 API, 그리고 좀 더 기능이 확장되고 구조체를 인자로 받는 신 API가 섞여서 중구난방스러운 것도 어쩔 수 없는 일이다. 가령, 예전에는 CheckMenuItem 같은 함수가 있었지만 지금은 SetMenuItemInfo가 있는 식. 새로운 함수는 범용적이긴 하지만 매번 구조체를 만들어서 초기화해 주는 작업이 몹시 성가신 것도 사실이다.
32비트 Windows부터는 각각의 메뉴 아이템에 대해서 명령 ID와는 별개로 임의의 UINT_PTR 데이터 값을 갖는 게 가능해졌다. 마치 리스트박스에서 item data와 비슷한 맥락이다. 이 값을 읽고 쓰는 함수로 지저분하게 SetMenuItemData 같은 함수를 또 추가하느니, 차라리 메뉴와 관련된 모든 속성을 읽고 쓸 수 있는 SetMenuItemInfo라는 종결자 함수를 만들게 됐을 것이다.
Posted by 사무엘