요즘은 컴퓨터의 인터넷 접근성이 워낙 좋아져서 응용 프로그램의 도움말은 그냥 개발사의 웹페이지에 기재된 문서 링크를 여는 걸로 대체하는 경우가 많다. 그러나 사용자의 컴퓨터에 직접 저장되어 있는 형태의 도움말 시스템도 여전히 필요하며 수요가 있다.

Windows가 98 시절부터 도입한 CHM, 즉 HTML 도움말은 여러 HTML 문서와 그림들을 한 파일로 묶어서 단일 컬렉션 파일을 만들 수 있다. 그렇기 때문에 소프트웨어의 도움말뿐만이 아니라 웹 문서 아카이브로도 활용할 수 있고 대단히 유용하다. 그 잠재적 유용성에 비해서 MS가 이 기술을 너무 홀대하고 있다는 생각이 든다.

평소에야 HtmlHelp 함수를 호출할 때 부모 윈도우의 핸들로 내 창을 넘겨 주면 알아서 도움말 창이 잘 생성된다. 그런데 내 프로그램은 별도로 창을 만들지 않으면서 HTML 도움말만 띄우고 싶으면 어떻게 하면 좋을까?
가령, 프로그램을 /?라는 인자를 주고 실행하면 옵션 사용법 도움말만 HTML 도움말 형태로 나온 뒤 프로그램을 바로 종료하게 하고 싶을 때 말이다.

일단, 운영체제는 HH.EXE라고 간단히 HTML 도움말을 띄워 주는 껍데기 프로그램을 제공하며, CHM 확장자는 기본적으로 이 프로그램에 연결되어 있다. 그렇기 때문에 ShellExecute 함수로 내 도움말 파일을 "open" 구동을 하면 도움말이 바로 뜨긴 한다.

그러나 이 방식은 도움말을 띄우는 것 자체 말고는 도움말 창에 대해서 그 어떤 제어도 할 수 없다. 가령, index.htm 같은 기본 시작 화면이 아니라 도움말 파일 내부에 있는 특정 문서를 바로 열게 하고 싶으면 도움말을 열지 말고 HH.EXE를 열고, 옵션에다가 xxxx.chm::/yyyy.htm 같은 식으로, chm 파일과 내부의 문서 파일을 이어서 특이하게 줘야 한다.

또한, HH.EXE의 실행이 끝날 때까지 기다렸다가 다른 후속 처리를 하게 하려면 이 프로세스의 핸들을 얻어야 할 텐데, 그러려면 ShellExecute보다 사용하기가 훨씬 더 까다로운 CreateProcess를 써야 할 것이다.

사실, WinHlp32.exe로 구동되던 과거의 HLP 도움말과는 달리, HTML 도움말은 hhctrl.ocx라는 DLL을 통해 in-process로 구동된다는 큰 차이가 있다. 이 특성을 살려, 굳이 외부 껍데기 프로세스인 HH.EXE를 호출하지 않고 내 프로세스가 직접 HTML 도움말 창 하나만 띄웠다가 곱게 종료할 수는 없을까?

부모 윈도우에다가 NULL을 주고 그냥 HtmlHelp 함수만 호출한 뒤 프로그램을 종료해 버리면, 도움말 창이 한 0.1초가량 눈에 비쳤다가 곧바로 사라져 버린다.
이 함수는 도움말 창을 띄워 주는 CreateWindowEx 함수와 개념상 거의 같다고 생각하면 된다. 이 함수도 생성된 도움말 창의 핸들값을 되돌리며, 창을 만든 뒤에는 그 창을 실제로 동작하게 하는 message loop을 돌려 줘야 한다.

HWND hMyWnd=::HtmlHelp(NULL, _T("xxxx.chm"), 0, 0);
ASSERT(hMyWnd!=NULL);

MSG m;
while(::GetMessage(&m,NULL,0,0)>0) {
    ::TranslateMessage(&m); ::DispatchMessage(&m);
}

이렇게 하면 도움말 창이 나타나긴 하나..
이번엔 도움말 창을 닫아도 프로그램이 종료되지 않고 '작업 관리자'에 내 프로세스가 언제까지나 표시되어 보인다는 문제가 발생한다.

내가 직접 창을 띄우고 윈도우 클래스를 등록하고 윈도우 프로시저를 구현하였다면, WM_DESTROY 메시지에서 응당 PostQuitMessage 함수를 호출해 줘서 GetMessage가 while문을 종료하게 했을 것이다.
그러나 도움말 창은 일반적으로 닫는다고 해서 응용 프로그램을 종료시키는 용도로 쓰는 물건이 아니다. 그래서 도움말 창만 단독으로 띄울 때 이런 문제가 생기는 것이다.

HTML 도움말 창이 없어질 때 프로그램도 정상적으로 종료되게 하는 방법은 크게 두 가지이다.
첫째는, 도움말 창이 WM_DESTROY 메시지를 받는 시점을 우리 프로그램이 잡아내어 그때 인위로 PostQuitMessage 함수를 호출하는 것이다. 훅킹(SetWindowsHookEx) 또는 서브클래싱(SetWindowLongPtr)을 생각할 수 있는데, 훅킹까지 쓰는 건 너무 오버인 것 같고, 내 경험상 이럴 때는 WM_DESTROY에 대해서 추가 처리만 살짝 해 주는 서브클래싱이 무난하다.

일반적으로 서브클래싱은 대화상자 안에 있는 각종 자식 컨트롤들의 동작을 미묘하게 바꾸기 위해서 하는데, 이렇게 큼직한 프레임 윈도우도 서브클래싱이 가능하다. 뭐, 서브클래싱을 쓰든 훅킹을 쓰든 어쨌든 콜백 함수를 정의해 줘야 하고 콜백 함수에게 context를 제공하기 위한 전역 변수나 TLS 슬롯이 필요하니 일이 여러 모로 복잡해진다.

다음 둘째는 첫째보다 더 정석적인 방법이다.
사실은 HTML 도움말 시스템 자체에, 도움말 창이 종료될 때 WM_QUIT 메시지를 보내게 하는 옵션이 있다. 딱 한 번만 옵션을 지정해 주고 나면 뒤끝 없이 OK이고 훅킹이고 뭐고 같은 지저분한 루틴이 없으니 아주 좋다. 그러나 옵션을 지정해 주는 방법이 생각보다 굉장히 지저분하다. API가 좀 구리게 설계되었다.

HH_WINTYPE hwt, *pwt=NULL;
::HtmlHelp(NULL, _T("xxxx.chm>main"), HH_GET_WIN_TYPE, (DWORD_PTR)&pwt);
if(pwt) {
    hwt=*pwt;
    hwt.fsValidMembers=HHWIN_PARAM_PROPERTIES;
    hwt.fsWinProperties=pwt->fsWinProperties|HHWIN_PROP_POST_QUIT;
    ::HtmlHelp(NULL, NULL, HH_SET_WIN_TYPE, (DWORD_PTR)&hwt);
}

이미 도움말 창이 떠 있는 상태에서 HtmlHelp 함수를 또 호출한다. 그런데, 도움말 창에 대한 정보를 얻기 위해서 창 핸들을 넘기는 게 아니라 또 도움말 파일을 길게 지정하고(중복 과잉 정보 공급), 그 뒤에 창의 내부 이름을 지정해 줘야 한다. 창의 내부 이름은 그 도움말 파일을 만든 사람이 지정해 준 명칭이다(저 예에서는 main).

핵심은 property에다가 HHWIN_PROP_POST_QUIT라는 속성을 추가로 지정해 주는 것이다. 이 상수는 불행히도 MSDN에 제대로 문서화도 돼 있지 않은 완전 잉여이다. 덕분에 이 명칭으로 구글링을 해도 수 페이지에 걸쳐서 이 이름의 값이 선언된 헤더 파일만 잔뜩 걸려 나올 뿐, 더 자세한 설명은 사실상 존재하지 않는다. HTML 도움말을 이런 식으로 깊숙하게(?) 다룰 생각을 하는 사람도 없을 테고 말이다.

나도 htmlhelp.h 파일을 뒤지다가 이걸 정말 우연히 발견했다. 그래도 이걸 써 주니 도움말 창을 닫을 때 프로그램이 바로 종료되게 할 수 있었다. Windows 98부터 8까지 다 잘 동작한다. HTML 도움말을 만든 개발팀에서 이 도움말 창만 단독으로 뜨는 상황도 생각을 안 한 건 아니었던 것이다.

공용 컨트롤을 다루면서 LVITEM 같은 구조체를 다룬 경험이 있는 분이라면, 저건 API 설계가 좀 특이하다는 걸 알 수 있을 것이다. 보통은 구조체를 선언하고, 구조체의 크기(t.cbSize=sizeof(t))와 얻고 싶은 정보를 나타내는 비트 플래그를 지정한 뒤, 구조체의 주소를(&t) get 함수에다 넘겨 준다.

그런데 HtmlHelp의 GetWinType는 아예 내부 포인터를 받게 돼 있다.
그리고 내가 지정하는 값은 property밖에 없음에도 불구하고 set을 할 때 일단은 구조체의 모든 멤버들의 값을 넘겨 줘야 한다(hwt=*pwt). 안 그러니까 프로그램이 에러가 나더라. 여러 모로 형태가 이상하다.

사실, HTML 도움말에는 저런 옵션을 지정할 필요가 없이 부모 윈도우에다가 여러 이벤트를 알려 주는 기능이 있다. 도움말 창이 처음으로 뜰 때(HHN_WINDOW_CREATE), 각종 페이지 이동 버튼을 누를 때(HHN_TRACK), 어떤 페이지를 성공적으로 열었을 때(HHN_NAVCOMPLETE) 이렇게 세 개가 정의되어 있는데, 사용자가 X 버튼을 눌러서 도움말 창이 소멸하는 시점을 알려 주는 기능이 없는 것은 개인적으로 굉장히 뜻밖이다. 왜 정작 필요한 이벤트는 없는 걸까? 본인이 개인적으로 가장 직관적으로 생각한 형태는 이런 것이었는데 말이다. 물론, 메시지를 받으려면 나도 윈도우를 하나 만들어야 하는 번거로움이 있긴 하지만 말이다.

EXE의 형태로 독립적으로 돌아가는 응용 프로그램이 아니라 DLL 형태인 IME들도 도움말을 표시하는 기능이 있다. 그러나 IME들은 안정성이나 키보드 포커스 같은 이유로 인해, 또 다른 DLL을 주입시키는 HtmlHelp 함수를 호출하는 게 아니라 앞서 소개했던 HH.EXE 프로세스를 수동으로 띄우는 원시적인 방식을 사용한다.
그래서 도움말 명령을 여러 번 내리면 도움말 창이 한도 끝도 없이 여러 개 생기며, IME를 사용하는 응용 프로그램을 종료하더라도 도움말 창은 같이 없어지지 않는다. Microsoft가 제공하는 기본 한중일 3개 국어 IME들이 모두 그렇게 동작하며, <날개셋> 한글 입력기 역시 외부 모듈은 그 관행을 따르고 있다.

본인을 포함해 HTML 도움말을 사용하는 많은 개발자들이 잊고 사는 사항인데, HTML 도움말도 원래는 사용 전에 초기화가 필요하다. HH_INITIALIZE 및 HH_UNINITIALIZE를 해 줘야 하고, 심지어는 message loop에다가도 원래는 HH_PRETRANSLATEMESSAGE를 해 줘야 한다. 하지만 현실적으로 그런 것까지 신경 쓰는 프로그램은 거의 없다. in-process 형태인 대신에 WinHelp 시절보다 번거로운 게 많아졌으며, IME의 경우 그런 것을 응용 프로그램에서 다 기대할 수 없으니 도움말을 외부 프로세스 형태로 실행해 주는 게 실제로 더 안전할지도 모르겠다.

HTML 도움말은 다형성을 지닌 인자에다가 typecasting을 하면서 여러 명령을 전달한다는 점, 초기화 및 해제가 필요하고 state를 지닌 변수가 존재한다는 점 등으로 인해 나름 클래스 라이브러리로 만들기에 적절한 면모가 있다. 물론 이 클래스의 인스턴스는 딱 단일체(singleton) 형태로만 존재해도 충분할 테고. 앞서 언급했던 자체 message loop을 도는 기능 역시 이 클래스의 멤버 함수로 추가해서 제공하는 것도 디자인 차원에서 생각해 볼 만하다.
이 글에서는 어쩌다 보니 HTML 도움말 하나만으로 일반적인 Windows 프로그래밍 이슈를 비롯해 다양한 이야기가 나왔다. ^^

Posted by 사무엘

2013/05/21 08:30 2013/05/21 08:30
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/833

Trackback URL : http://moogi.new21.org/tc/trackback/833

Comments List

  1. nyam 2013/05/21 09:54 # M/D Reply Permalink

    늘 깊이있는 지식을 공유해주심에 감사드립니다.
    저도 요즘 프로그램들이 인터넷 연결을 당연시하고 있다는 말씀에 동의합니다..
    그래도 도움말의 특성상 사용자들은 빠른 시간 안에 자신이 원하는 것에 대한 정보를 얻어내기를 원할텐데
    로컬에 저장되어있어 빠르고 일반적으로 목차가 함께 제공되어 직관적으로 접근 가능한 CHM은 이에 분명 아직도 장점이 많다고 생각됩니다.

    사실 그 이전에 정말 빠릿빠릿하게 열리는 HLP에 대해 Microsoft가 운영체제에서 WinHlp32.exe를 제거(사실 제거라기보다는 junk 프로그램을 넣어놨죠..)한 것도 전 매우 아쉽다는 생각이 듭니다.
    굳이 다양한 HTML의 기능을 사용하지 않는다면 HLP 만으로도 충분하겠죠..

    헌데 사실 뜯어보면 htmlhelp.lib 안의 코드도 참 재미있더군요..
    HtmlHelpA(W)에 대해 static하게 link하는 정보를 가진 것이 아니라
    LoadLibrary를 사용해 동적으로 hhctrl.ocx를 로드해서 GetProcAddress로 HtmlHelpA(W) 함수의 주소를 얻어와서 호출하더군요.
    제 생각으로는 HTML 도움말(hhctrl.ocx)이 설치되어있지 않은 과거 Windows 시스템에서 hhctrl.ocx의 static link로 인한 전체 프로그램 실행 불가능 사태를 방지하고자 했던 것 같은데 나름의 배려가 느껴지더군요..
    제 판단이 틀렸을 수도 있겠지만요.. ^^;;

    어쨌든 HLP를 사용하다가 CHM을 쓰면서.. 속도나 접근성 면에서 불만을 가질 때가 엊그제 같은데
    이제는 CHM도 점점 그리워지는 상황이 오고 있는 것 같습니다.. ^^;;

    1. 사무엘 2013/05/21 18:47 # M/D Permalink

      의견 남겨 주셔서 고맙습니다. ^^
      WinHelp는 Windows 3.0과 함께 처음 도입되었을 때는 정말 획기적이고 엄청난 물건이었지 싶습니다.

      운영체제 API에 정식으로 함수가 등재될 정도였고, Windows 95/98의 컴퓨터 시절에는 CHM보다 속도도 넘사벽급으로 빠르고 가볍고 좋았지요. 하지만 HLP도 ActiveX나 다름없는 각종 플러그 인을 얹고 구조를 확장할 수 있다는 점이 보안이라든가 64비트 대응 관점에서는 악재였던 것 같습니다. 옛날에 Robo HelpOffice던가 이런 툴을 쓰면 HLP로도 CHM 스타일의 목차 화면까지 만들 수 있었지요.

      그리고 HtmlHelp 함수는 말씀하신 것처럼 DLL로부터 함수 import를 직접 하는 게 아니라, hhctrl.ocx를 간접적으로 GetProcAddress하는 코드의 형태로 링크됩니다. 잘 알려진 사실이죠. HTML 도움말 시스템이 없는 IE 4 이하의 컴에서도 아예 로딩부터 실패하지는 않게 배려한 정책입니다. ^^;;;

Leave a comment
« Previous : 1 : ... 1475 : 1476 : 1477 : 1478 : 1479 : 1480 : 1481 : 1482 : 1483 : ... 2204 : 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:
3050254
Today:
1274
Yesterday:
2142