« Previous : 1 : ... 13 : 14 : 15 : 16 : 17 : 18 : 19 : 20 : 21 : ... 31 : Next »

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

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

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

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

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

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

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

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

Posted by 사무엘

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

1. WM_QUERYDRAGICON 메시지

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

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

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

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

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

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

2. 잉여 WM_SIZE 파라메터

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

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

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

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

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

Posted by 사무엘

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

예전에도 몇 차례 얘기했듯이 비주얼 C++은 지금까지 내 인생에서 가장 재미있는 장난감이요, 친구요, 자아실현 매체요, 생계 수단 역할을 톡톡이 해 왔다.

비주얼 C++은 여느 프로그래밍 툴과는 다르게 뭐랄까, standalone, independent이고 자가생성이 가능하다. 쉽게 말해서 비주얼 C++ 자신과 같은 레벨의 컴파일러/런타임/IDE 같은 프로그램을 비주얼 C++로 또 만들 수 있다는 뜻이다. 실제로 마소에서 비주얼 C++은 이전 버전의 비주얼 C++로 만들고 있기도 하고. 이렇듯, 이 툴은 가장 배우기 어렵지만 가장 강력하고 군더더기 없는 프로그램을 만들 수 있다.

2014년 현재, 난 한 컴퓨터에 다음과 같은 세 버전을 깔아 놓는다. 제각기 필요와 쓸모가 있기 때문이다.

1. 2003

  • 2010에서 새로 도입된 Help Viewer가 완전 거지 같아서.. 단순 윈도 API나 MFC 레퍼런스를 조회하는 덴 200x 구버전 document explorer 기반의 msdn이 짱이다. (1) 색인이 처음에 뜨는 데 시간이 너무 오래 걸리는 것, (2) 가끔 목차/색인을 클릭해도 해당 항목 문서가 안 나타나는 것--정확히는 수 초 뒤에 한참 뒤에 뜸.. 이 두 버그 때문에 학을 뗐다. (단, 2012 이후의 Help Viewer 2.0은 불편하던 게 좀 개선된 거 같기도 하고..)
  • 2003은 MFC가 지금처럼 말도 안 되게 bloat되기 전이며, 굉장한 legacy 운영체제에도 돌아가는 바이너리를 만들 수 있는 버전이다. <날개셋> 타자연습을 여전히 10년 전의 구닥다리 컴파일러로 빌드하는 이유가 이것 때문이다.
  • 다만 2003은 IDE가 빌드 내지 리소스 편집 중에 잘 뻗는 편이고(불안정!) Vista 이후 OS에서는 일부 기능이 충돌도 함. 조심해서 써야 한다.

2. 2010

  • 닷넷 이래로 Visual Studio가 기본 제공하던 msi 설치/배포 프로젝트 기능이 2012에서 갑자기 없어져 버린 관계로, 2010을 도저히 제거할 수가 없게 됐다. 대체품이라는 InstallShield 번들 에디션은 어마어마한 덩치와 복잡한 사용법 때문에 곧바로 gg 치고 언인스톨해 버렸다.
  • 또한 <날개셋> 한글 입력기는 빌드와 관련된 특이한 이슈 때문에 2012가 아닌 2010 컴파일러 툴체인을 사용하고 있다.
  • 다만, 2010은 IDE의 비주얼이 역대 VC++ 역사상 제일 구리고 우중충 칙칙하고 안 좋았다. -_-;;

3. 2012

난 201x가 다음과 같은 점에서 마음에 든다. (1) 크게 강화된 인텔리센스 엔진 (2) 람다 같은 C++ 최신 문법 (3) 빌드나 리소스 편집 중에 IDE가 이제 거의 뻗지 않음
2012는 이를 바탕으로 2010보다 훨씬 더 깔끔한 GUI에, 신택스 컬러링도 훨씬 더 강화되어 몹시 마음에 든다. 몇 가지 크리티컬만 없었으면 2012가 2010을 완전히 대체할 수도 있었을 텐데. ㅜ.ㅜ
다만 2012 얘만 꼭 남겨 둘 이유 역시 없기 때문에 이것보다 더 최신 버전이 나오면 그걸로 대체할 수도 있다. 즉, 2012는 2003/2010과는 달리 고정 보존 상태는 아니다.

위와는 달리, 보존 대상에서 제외되고 안 쓰는 버전은 다음과 같다.

1. 6.0

VC6은 그야말로 개발툴계의 IE6이나 마찬가지다. 출시 시기는 다르지만 공교롭게도 버전 번호도 동일하고 말이다. IE가 윈도 비스타의 출시 지연 때문에 6 이후로 5년 가까이 버전업이 없었다면, VC는 닷넷이 첫 개발되느라 4년 가까이 6 이후로 버전업이 없었다. 그 뒤 지나치게 오랫동안 현역을 뛰어 왔다.

웹 개발자들이 제발 IE6 좀 퇴출시키자고 캠페인 하는 것만큼이나 PC 클라이언트 개발자들은 업계에서 VC6 좀 퇴출시키자고 캠페인이라도 해야 할 판이다. 단지, IE는 모든 PC 사용자들이 쓰는 웹브라우저인 반면, VC는 극소수 프로그래머만이 쓰는 개발툴이라는 점이 다르다.

VC6은 이제 해도 해도 너무하다 싶을 정도로 심하게 후지고 낡았다. IDE가 IME-aware하지도 않고, 특히 한글 윈도에서는 기본 글꼴이 윈도 3.1 스타일의 완전 추레한 System으로 나옴! 인텔리센스는 지금에 비하면 완전 안습 크리 수준이고. 최신 C++ 표준이나 멀티코어 같은 건 아웃 오브 안중이다.

VC6이 아니면 도저히 빌드시킬 수 없는 비표준 코드가 이미 수십만 줄 이상 작성되어 버려서 도저히 수습을 못 할 지경이 된, 한 20년 묵은 불가피한 프로젝트가 아니라면 아직까지 VC6을 고집할 이유란 없어야 정상일 것이다. for문 변수 scope 정도는 후대의 컴파일러로도 옵션을 바꿔서 수용시킬 수 있을 텐데.

굳이 장점을 찾자면, VC6은 생성되는 바이너리가 운영체제의 MSVCRT와 MFC42를 직통 지원한다는 점이 매우 유리하다. 그러나 이것도 어차피 64비트는 지원 안 하기 때문에 장점이 반쪽짜리 이하로 의미를 크게 상실한다.

2. 2005

MS 오피스 2003이 아닌 독자 GUI 비주얼을 선택한 첫 버전 되시겠다. (VC 2005가 오피스 2003 같은 시퍼런 비주얼 기반이었다면? 상상만 해도 ㅎㅎ)
난 얘는 일단 sp1과 운영체제 패치를 설치하는 시간이 2005 자체를 설치하는 데 걸리는 시간보다 더 길어서 인상이 매우 안 좋다. 게다가 CRT/MFC DLL 배포 방식도 구리게 바뀌었고. 장점은 어차피 (1) 2003이나(msdn 등) (2) 이후 버전(64비트 지원 등)에 다 포함돼 있기 때문에 굳이 얘가 필요하진 않다. out.

3. 2008

2005보다는 훨씬 더 괜찮은 물건이고 쓸 만하다. 그리고 은은한 연보라색 톤(비스타/7 기준)의 IDE 외형은 역대 버전들 중 가장 깔끔하고 괜찮았다고 생각한다.
200x 중에서는 가장 훌륭했지만, 역시 얘만 보존해야 할 필요는 존재하지 않는다. 플러스 팩의 등장과 함께 MFC가 완전 bloatware로 바뀌어 버렸고, CRT/MFC DLL 배포 방식은 여전히 아쉬운 점이다.

위의 두 카테고리 말고 본인이 special case로 예우하는 골동품 버전이 있는데, 그건 6.0보다도 더 옛날 버전인 4.2이다. mfc42의 원조인 바로 그 버전이다.
본인이 난생 처음으로 구경한 비주얼 C++ 버전이어서 애착이 간다.

Posted by 사무엘

2014/08/07 08:28 2014/08/07 08:28
, , ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/993

C/C++에서 구조체나 클래스는 통상적으로 global scope에서 선언되거나 기껏해야 다른 클래스 내지 namespace의 내부에서 선언된다. 즉, 어차피 비실행문들만 있는 곳에서 선언되는 편이다.
그러나 실행문으로 이뤄져 있는 함수 안에서 이들을 새로 선언하는 것도 문법적으로 가능하다. 다시 말해, 변수를 선언하는 것뿐만 아니라 그 변수들의 type을 결정하는 구조체나 클래스를 즉석에서 선언해 쓰는 것도 가능하다는 뜻이다.

다른 곳에서 두고두고 재사용하는 구조체가 아니라 함수 한 곳에서 튜플 같은 형태로 잠깐만 사용하고 마는 구조체라면, 이런 식으로 함수 안에서 간단히 선언해서 사용하면 좋다. 하긴, 그러고 보니 struct, class, union, enum뿐만 아니라 typedef도 실행문과 비실행문 문맥에서 모두 사용 가능한 물건이다.

함수 안에서 클래스 같은 걸 따로 선언하는 건 C#에서는 가능하지 않으며 C++만의 전유물인 듯하다.
이렇게 함수에서 선언된 자료형은 유효 범위도 마치 지역변수처럼 그 함수 안으로 완전 local하게 한정된다. 그래서 각종 IDE 같은 데에 명칭이 뜨지도 않는다. 지금부터는 이와 관련해서 좀 더 기괴한 이야기들을 풀어 보겠다.

1. 무명 자료형

C/C++에는 '이름 없는' 구조체/클래스/공용체 따위의 개체가 있을 수 있다. '이름 없는' 함수 그 자체만의 선언은 지원되지 않아서 함수형 프로그래밍 패러다임이 도입된 C++0x 이후에서야 람다와 함께 등장한 반면, 이름 없는 복합 자료형이라는 개념은 있었던 것이다.

class {
public:
    int x,y,z;
} obj;

C#이나 자바 스타일이라면 상상도 할 수 없는 일이겠지만, C/C++은 자료형의 선언과 해당 자료형에 속하는 변수의 선언을 동시에 할 수 있다. class OBJ { ... } a,b,c; 도 OBJ a,b,c;나 심지어 int a,b,c;와 개념적으로 같다.
class, struct, enum, union 등을 선언한 뒤에는 닫는 중괄호 다음에 세미콜론을 반드시 붙여야 하는 이유가 바로 이 때문이다.

그런데 이름 없는 자료형은 자료형의 선언과 함께 변수 선언도 같이 해 주는 게 선택이 아닌 '필수'라는 차이가 있다. 그도 그럴 것이, 얘는 이름이 없는 일회용 자료형인 고로 그 자료형을 선언하는 구문이 끝난 뒤에는 그걸 지칭할 방법이 없기 때문이다. 변수가 단 하나라도 같이 선언돼 있어야 나중에 C++11의 auto 라도 써서 그것과 동일한 자료형의 변수를 추가로 만들 수 있을 것이다.

이런 무명 자료형이라는 개념은 대개 한 자료구조 내부에서 구조체와 공용체를 섞어 가며 쓸 때 유용하지만, 그렇잖아도 일회용 성격이 강한 local 자료형에서도 더욱 의미가 있다. 굳이 이름을 생각할 필요 없이 내가 생각하는 복합 자료형을 간단하게 만들어서 쓰게 해 주기 때문이다.
물론 local뿐만 아니라 global scope에서도 무명 자료형을 얼마든지 선언해서 쓸 수 있다. C/C++의 오묘한 면모 중 하나이다.

2. 함수 안에 함수

C/C++은 복합 자료형은 앞서 살펴보았듯이 무명으로 선언할 수 있고, 그 안에 또 다른 복합 자료형을 nested된 형태로 선언하고 집어넣을 수 있다. 그러나 실행되는 코드의 집합인 함수를 그렇게 일종의 값처럼 자유자재로 다룰 수 있지는 않았다.

함수 자체를 다른 함수에다가 전달하는 것은 그나마 함수 포인터가 있으니 불가능하지는 않지만, 그건 자료형, 함수명 등에 대한 작명이 필요하며 기계 중심적이고 융통성이 부족했다. 또한 함수 안에다가 또 일회용으로 간단히 쓰고 마는 함수를 잠깐 선언하는 것도 가능치 않아서 global/class scope 차원에서의 선언이 필요했다. 남는 건 매크로 함수밖에 없지만 이게 얼마나 구조적으로 허접한 물건인지는 역시 설명이 필요하지 않는 수준이고.

void func()
{
    void simple_func(int x) { }

    simple_func(0);
    simple_func(1);
}

nested function은 C와 파스칼의 큰 차이 중 하나이기도 했다. 파스칼은 지원하지만 C/C++ 계열은 지원하지 않았기 때문이다. 마치 가변 길이 배열만큼이나 언어 차원에서 결코 지원되지 않을 금기 봉인 사항이기라도 한가 궁금하다. 다만, 옛날에 gcc던가 극소수 C 컴파일러에서 확장 옵션을 통해서 nested function을 지원하는 걸 본 것 같다.

물론, 중첩 함수를 써서 할 수 있는 일은 중첩 함수라는 개념이 없이도 완전히 똑같이 할 수 있기 때문에 상호 등가 교환이 가능하다. 마치 클래스에서 public과 private 구분을 해 주든, 아니면 전부 싸잡아 public인 struct로 코드를 작성하든.. 이것은 코드의 유지 관리의 편의성 내지 정보 은닉하고만 관계가 있지 프로그래밍 언어의 구조적인 계산 능력과는 무관한 것하고 같은 맥락이다. 그래서 C/C++은 nested 함수라는 개념을 도입하지 않은 듯하다. 정수 타입에 subrange 같은 개념도 없을 정도이니 뭐~

지금이야 람다 덕분에 함수 안에 함수의 선언이 사실상 가능해졌다. 캡처 같은 새로운 개념도 같이 도입됐다. 하지만 이건 일반적인 함수와 개념적으로 같은 물건은 아니다.
C++에서는 (1) 중첩 namespace 안에 들어있는 함수가 얼추 비슷한 개념일 수 있으며, 이것 말고도 좀 더 직접적으로 함수 안에 함수를 만드는 것이 편법 우회 경로로 가능하다. (2) 바로 함수 안에 클래스를 선언하고 멤버 함수를 정의하는 것이다. 이런 식으로.

int main(int argc, char* argv[])
{
    class A {
    public:
        static void Func() { puts("function inside function"); }
    };
    A::Func();
    return 0;
}

특히 static 함수는 this 포인터를 사용하지도 않으니 진짜로 일반 함수와 다를 바가 없다.
함수 안에다 구조체를 정의하는 것으로도 모자라서 완전한 형태의 클래스를 정의하고 멤버 함수를 정의하는 것까지도 가능하다니 놀랍지 않은지?

단, 이런 지역 클래스에서 멤버 함수를 선언할 때는 논리적으로 당연한 제약이 하나 걸린다. 함수의 몸체는 반드시 그 클래스 안에서 저렇게 정의되어야 한다. 안 그러면 아까 무명 자료형에서 변수 선언을 바로 안 해 줄 때처럼 경고가 뜬다.

비주얼 C++의 경우 일단 C4822 경고만 뜨고 그걸 실제로 호출까지 한 경우 링크 에러가 났지만, 요즘은 그 즉시 C3640 에러도 같이 나오는 듯. 링크 에러가 더 친절하게 컴파일 에러로 바뀌었다.
클래스의 밖인 그 함수 몸체 안에서 또 void A::Func() { } 이런 식으로 함수 몸체를 따로 정의하는 건 문법적으로 허용되지 않기 때문이다.

또한, 이런 이유로 인해, 지역 클래스는 static 멤버 함수는 가질 수 있는 반면 static 멤버 변수(=데이터)는 가질 수 없다.
그건 함수 안의 일반 static 변수와 같은 취급을 받으려나 궁금했는데, 만들어 보니 그건 언어 문법 차원에서 허용되지 않으며 곧바로 컴파일 에러가 난다. static const도 허용되지 않는다.

그러고 보니 이름 없는 클래스도 static 멤버 변수를 사실상 가질 수 없을 듯하다. 사실, 이름 없는 클래스에다가 그런 것까지 바라는 것 자체가 변태 도둑놈 심보이긴 하다. ㅎㅎ
멤버 함수야 몸체를 클래스의 선언부 안에다 강제로 집어넣는 식으로 정의할 수 있지만 static 변수는 결국 클래스 밖에서 따로 정의를 해야 하는데, 클래스 이름이 없으니 정의를 할 수 없어서 링크 에러가 나기 때문이다.
이거 정말 복잡한 문제다. C++이 C#/Java하고는 다른 독특 기괴한 면모가 이런 데서 또 발견된다.

Posted by 사무엘

2014/07/25 08:32 2014/07/25 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/988

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

CStackCtrl m_wndCustomCtrl;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

컴퓨터 화면에 그려진 어떤 물건(주로 사각형 모양)의 경계 테두리에다 마우스를 갖다대면 포인터가 그 경계 테두리와 수직인 방향의 화살표로 변한다. 그 상태에서 마우스를 클릭하여 끌면, 그 물건의 크기를 마우스 포인터가 움직이는 방향으로 변경할 수 있다.
이것은 GUI에서 매우 흔히 볼 수 있는 기능이다. 특히 크기 조절 가능한 창--운영체제가 정식으로 제공하는 GUI 구성요소--의 경우 이런 기능은 운영체제가 non-client 영역에서 알아서 자동으로 처리해 준다.

그런 창이야 운영체제가 처리를 알아서 해 주지만, 대화상자 내부의 임의의 영역에 대해서, 혹은 클라이언트 영역에 내가 그려 주는 임의의 객체에 대해서 이런 처리를 구현하려면 어떻해야 할까? 뭔가 공통된 패턴의 알고리즘을 처리해야 할 텐데 코딩량이 적지는 않으며 왠지 귀찮고 번거로워 보인다.

크기 조절 내지 화면 분할 UI와 관련해서는 다음과 같은 여러 상황을 생각할 수 있다.

1. 한 윈도우 내부에 그려지는 개체의 크기 조절

2차원 벡터 그래픽 프로그램 내지 RAD 툴의 폼 에디터가 정확하게 여기에 속한다. 요즘은 워드 프로세서도 자체적인 벡터 그래픽이나 하다못해 OLE 개체라도 취급하니 마우스를 이용한 크기 조절 기능을 제공해야 한다. 개체를 클릭하면 8군데의 앵커 사각형이 생기며, 경계 아무 곳이라기보다는 그 앵커 사각형을 드래그했을 때 크기 조절이 된다. 다른 곳을 드래그하면 크기 조절이 아니라 이동이 되고. 그리고 요즘 MS 오피스 제품은 회전용 앵커까지 덤으로 제공한다.

비주얼 C++에는 CRectTracker라는 고전적인 클래스가 있어서 마우스 드래그로 임의의 사각형 영역을 화면에다 그리고 마우스가 클릭됐을 때의 일체의 처리를 알아서 해 준다. 이것만 쓸 줄 알아도 상당히 편리한데, 본인은 지금까지 그런 분야의 프로그램을 개발할 일이 없다 보니 실제로 써 본 적은 전혀 없다. 이런 게 있다는 것만 안다.

그리고, 지금도 있나 모르겠다만, 비주얼 C++에는 DrawCli라고 딱 개체 기반 벡터 드로잉과 드래그 드롭, 크기 조절과 이동을 모두 시연해 놓은 걸출한 예제 프로그램이 있다.
학창 시절 나의 친구였던 <비주얼 C++ 완벽 가이드>(김 용성)에도 '트래커'라는 예제 프로그램이 있으니 참고할 것.

사용자 삽입 이미지

참고로, 저렇게 테두리를 그리고 사각형 안의 내용물은 대각선 사선으로 칠하는 건.. embed된 OLE 개체를 외부 프로그램이 수정 중일 때 클라이언트 프로그램이 표시하는 표준 모양이기도 하다.

사용자 삽입 이미지

2. 부모 윈도우가 자식 윈도우를 동적으로 분할하고 관리

규모깨나 좀 있는 문서 편집 프로그램을 보면, view를 분할하는 기능이 있다. 이것은 한 문서 컨텐츠에 대해 뷰 윈도우를 여러 개 둬서 한 컨텐츠를 여러 가지 다른 방식, 다른 위치를 표시할 수 있게 한다. 사용자의 입장에서 매우 편리한 기능이지만, 컨텐츠와 뷰 윈도우가 완전히 일심동체라고 전제하고 만들어져 버린 프로그램이라면 추후에 이러 기능을 추가하기가 쉽지 않을 것이다. MDI 프로그램이라면 아예 별도의 독립된 창을 만드는 기능도 있겠지만 SDI에서는 한 창을 분할하는 것만 가능하다.

이건.. 생각보다 만들기 어려운 기능이다. 경계(splitter) 부분을 마우스로 끌었을 때의 처리도 처리거니와--이를테면 XOR 연산으로 자취를 그렸다가 지우는 것도..--, 내가 무엇보다도 힘들겠다고 느끼는 건 스크롤 바를 자체적으로 따로 만들어서 관리하는 부분이다. 이게 무슨 뜻인지를 설명하자면 이렇다.

Windows 운영체제에는 어떤 윈도우가 자체적으로 스크롤 바를 가질 수 있고, 한편으로 스크롤 바 자체가 별도의 컨트롤로 존재할 수 있다.
윈도우가 자체적으로 갖는 native 스크롤 바는 당연히 운영체제가 모든 처리를 알아서 해 준다. 창의 크기가 바뀌어도 스크롤 바를 자동으로 우측(상하) 내지 하단(좌우)에 배치해 주고, 스크롤을 할 필요가 없어지면 알아서 스크롤 바가 없어지고 그 영역까지 클라이언트 영역이 확대된다.

그러나 splitter가 존재하는 view를 보면, 스크롤 바가 있던 자리의 구석 일부에 창을 분할시키는 앵커가 자리잡고 있다. 이건 native 스크롤 바로 구현 가능하지 않기 때문에 스크롤 바 컨트롤을 따로 만들어서 앵커 밑이나 옆에다 두고, 스크롤 바의 위치· 크기와 관련된 모든 처리를 수동으로 해야 한다. 이 얼마나 복잡하고 손이 많이 갈까? 그러니 MFC가 CSplitterWnd라는 클래스에다 전부 구현해 놨다.

사용자 삽입 이미지

MFC AppWizard에서 '뷰 분할 기능 사용'을 체크하면, 프레임 윈도우는 밑에다 저 splitter 윈도우를 생성하고 걔가 또 자기 밑에다 어떤 view를 생성할지를 따로 지정해 준다. 다시 말해 프레임 윈도우와 view 사이에 splitter라는 중간 계층 윈도우가 하나 또 생긴다는 것이다. 그리고 요놈이 스크롤 바와 앵커의 위치를 관리하고 앵커 드래그에 대한 처리도 담당한다.

사용자 삽입 이미지

내가 MFC의 splitter 윈도우에 대해 꽤 놀란 것은, 분할을 2개씩뿐만 아니라 그 이상도 얼마든지 할 수 있다는 점이다. 위의 그림처럼 가로 3개, 세로 3개를 분할해서 무려 9개나 되는 view 윈도우를 뻥튀기시킬 수도 있다. 가로와 세로의 splitter 중 어느 것 하나의 위치가 바뀌었을 때, 혹은 창 전체의 크기가 바뀌었을 때 splitter가 총체적으로 조율하고 해야 할 일의 양도 그에 비례해서 많아질 것이다.

그런데 각각의 창들이 다 독자적인 가로· 세로 스크롤 바를 갖는 건 아니고.. 위의 스크린샷에 보듯이 한 column에 해당하는 view들은 가로 스크롤 바를 하나 다같이 공유한다. 그리고 한 row에 해당하는 view들은 세로 스크롤 바를 다같이 공유한다. 특이한 점임.

3. 부모 윈도우가 자식 윈도우를 '정적'으로 분할하고 관리

위의 2번과 마찬가지로 부모 윈도우가 자식 윈도우의 분할을 관리하는 경우이긴 한데, 위처럼 성격이 비슷한 윈도우가 아니라 별개의 윈도우를 고정적으로 관리하는 경우를 추가적으로 생각할 수 있다. 처음엔 1개였다가 2개 이상으로 자유자재로 분할되는 건 아니기 때문에, 스크롤 바나 앵커 같은 건 없다.

왼쪽에 트리 컨트롤, 오른쪽에 리스트 컨트롤을 두고 가로로 크기 조절이 가능한 탐색기 같은 프로그램도 좋은 예이고, 그보다 좀 더 복잡한 경우로는 개발자들의 친구인 Dependency Walker가 있다.

사용자 삽입 이미지

Spy++ 같은 프로그램으로 들여다보면, 창 구조가 생각보다 복잡하다는 걸 알 수 있다.
가장 겉에 있는 창은 화면을 상하로 나눈다. 위에 있는 창은 화면을 또 좌우로 나눠서 왼쪽은 모듈 트리 구조가 나오며, 오른쪽은 또 상하로 나누어서 각각 이 모듈이 import하는 심벌들, 그리고 대상 모듈이 export 하는 전체 심벌들이 표시된다.
한편, 아래의 화면은 또 상하로 나뉘어서 위에는 전체 모듈 리스트가 있고 아래에는 메시지 log가 있다.

즉, 한 윈도우에 여러 개의 컨트롤들이 sibling 관계로 대등하게 늘어서 있는 게 아니라, 또 분할 윈도우가 있고 그 아래에 또 분할 윈도우가 자식 윈도우로 있는 형태다. 한 분할 윈도우는 언제나 좌우로든 상하로든 2개의 윈도우만을 담당한다.

이렇게 이분법적으로 접근하면, 제아무리 복잡하게 화면이 좌우 상하로 분할되어 있는 창이라 해도 전체 크기가 바뀌었다거나 할 때 자기가 맡은 두 개의 창만 비율 분배를 잘 하고 나머지는 자가반복적인 재귀 처리에 맡기면 되니 문제가 단순해진다는 장점이 있다.

저런 윈도우가 활용의 자유도가 더욱 올라간다면 아예 별도의 창으로 분리하거나 docking까지 가능해진다. Visual Studio의 각종 보조 윈도우들처럼 말이다. 그건 우리 같은 평범한 프로그래머가 밑바닥부터 할 짓이 못 되며, 이미 있는 GUI 라이브러리의 사용법을 익히는 것만으로도 충분할 것이다. MFC 없이 Windows API만으로 docking toolbar를 구현한 소스를 외국 사이트에서 본 적이 있는데, 가히 근성이 느껴졌다.

사용자 인터페이스라는 건 컴퓨터로 하여금 의미 있는 작업을 하게 만드는 실질적인 알고리즘이 아니다. 없던 걸 처음 시도하여 만드는 게 아닌 이상, 베끼기만 하느라 시간 낭비할 필요는 없을 것이다.

4. 대등한 위상의 윈도우끼리 분할 관리를 해야 하는 경우

어휴, 글을 이렇게 길게 쓸 생각은 없었는데... 마지막 아이템도 언급을 안 할 수가 없구나. -_-;;
자, 지금까지 얘기한 것들을 정리하자면 1번은 그냥 한 윈도우 내부에서 그리기를 하는 것뿐이며, 2번과 3번은 부모 윈도우가 자식 윈도우들의 공간 관리를 하는 경우를 특별히 MFC의 document-view 아키텍처의 예를 들어 소개한 것이다.

그러나 실무에서는 그것만이 전부가 아니다. 대화상자처럼 document-view 아키텍처가 적용되지 않는 창에 대해서도 수평/수직 splitter 같은 물건을 만들어야 할 때가 있다.
예를 들어, <날개셋> 제어판 같은 경우, '분야'를 나타내는 왼쪽의 트리 컨트롤과, 오른쪽의 여타 컨트롤들 사이에 수직 splitter를 둬서, 트리 컨트롤의 폭을 좀 더 넓힌다거나 반대로 좁히는 경우를 생각할 수 있다.
이 경우 splitter는 여타 컨트롤들과 마찬가지로 대화상자 안에 존재하는 여러 자식 윈도우의 하나일 뿐이지 나머지 컨트롤들을 모두 통솔하는 부모 윈도우의 지위는 아니게 된다. Spy++로 들여다보면, 가로나 세로로 길쭉한 고유한 splitter 윈도우가 잡히는 걸 볼 수 있다.

이런 상황에 대해서는 MFC는 딱히 제공해 주는 있는 클래스가 없다. codeguru 같은 데서 splitter dialog 정도로 검색해 보면 예제 소스나 관련 튜토리얼들이 쭉 나온다. 예전에 아주 괜찮은 코드를 하나 구해서 유용하게 쓴 적이 있었는데 지금 다시 검색하려니까 못 찾겠다.

이런 일을 하는 범용적인 클래스를 만들 때 염두에 둬야 하는 사항으로는, 좌우나 상하든 보장해 줘야 하는 최소 크기를 인자로 받아야 할 것이고, 좌우 상하 중 한쪽에다 뒀으면 하는 윈도우를 배열 같은 자료구조로 관리해야 한다. 윈도우 핸들이 아닌 ID로 받으면.. ID로부터 실제 핸들값(HWND)을 얻어야 하는 번거로움이 있지만, 그 컨트롤이 중간에 재생성된다거나 해도 여전히 식별이 가능하기 때문에 범용성이 좀 더 향상된다.

다른 splitter조차 자기의 크기 조절에 영향을 받게 하고 WM_SIZE 메시지에 반응한다면, 아까 Dependency Walker 같은 복잡다단 splitter도 얼마든지 구현 가능하다.
splitter를 구현하려면 당장 크기를 조절하는 것 처리는 둘째치고라도, 창의 크기가 바뀌었을 때 각 분할 화면들의 공간 배분을 어떻게 할지 같은 것도 생각해야 하니 여러 모로 골치가 아픈 건 사실이다.

끝으로 언급하고 싶은 이슈가 있다. 1~4번들은 다 마우스가 클릭되었을 때 캡처를 잡고 마우스 움직임을 추적하다가 버튼이 떼졌을 때 마무리 처리를 한다는 공통점이 있다.
이 경우, WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP을 모두 메시지 맵에다 등록하고 각 상황별 코드를 메시지 핸들러 함수에다 제각기 따로 작성하는 방법을 생각할 수 있지만..

좀 더 능숙한 프로그래머라면, 그런 드래그 드롭 처리 정도면 WM_LBUTTONDOWN에다가 아예 별도의 message loop을 만들어서 거기에다 WM_MOUSEMOVE와 WM_LBUTTONUP에 해당하는 코드를 다 집어넣는 방법을 선택한다. 한 함수에다가 한 기능에 대한 처리를 몰아서 넣는 게 훨씬 더 깔끔하기 때문이다.

Posted by 사무엘

2014/07/02 08:32 2014/07/02 08:32
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/980

하루는 본인은 회사 업무를 위해 인터넷에 굴러다니는 어느 암호화 알고리즘 소스를 프로젝트에다 붙여 쓴 적이 있었다.
그런데 곧장 문제가 발생했다. 본인이 맡은 부분은 Windows용 클라이언트인데, 같은 소스를 사용하는 다른 플랫폼 클라이언트 내지 서버와 교신이 제대로 되지 않고 있었다.

결국은 문제의 코드를 별도의 콘솔 프로그램 프로젝트로 떼어서 따로 돌려 보니, 문제의 원인은 그 암호화 알고리즘에 있음이 밝혀졌다. 같은 소스를 빌드해서 돌렸는데 결과가 서로 차이가 나는 것이었다.
게다가 Visual C++로 빌드하는 같은 Windows용 프로그램도, 알고 보니 debug 빌드는 결과가 옳게 나오는데 release 빌드만이 문제가 있었다!

debug와 release가 서로 다르게 동작하는 프로그램은 십중팔구가 멀티스레드 race condition 아니면 단순 초기화되지 않은 변수 때문이다. 물론 이 코드는 스레드를 따로 만들지는 않으니 의심 부분은 응당 후자. 이거 또 남이 짜 놓은 복잡한 코드에서 꼭꼭 짱박혀 있는 버그 찾느라 무진장 고생하겠다는 생각과 함께 몇 시간 동안 디버깅을 진행했다.

release 모드로 빌드된 프로그램은 함수 인라이닝과 각종 최적화 때문에 debug 빌드처럼 한 라인씩 엄밀하게 step in이 되지 않으며 변수값 조회도 안 되는 경우가 종종 있다. 그러니 도대체 언제부터 두 빌드의 변수값이 달라지는지 printf 신공을 펼치면서 꽤 어렵게 문제 원인을 추적해야 했다.

문제의 범위는 많이 좁혀졌다. stack이나 heap 메모리를 초기화하지 않고 쓴 경우는 눈을 씻고 찾아도 없었다. 마치 난수 씨앗처럼 초기의 동일한 input으로부터 일련의 output들이 계산을 통해 파생되는데, 언제부턴가 두 빌드가 생성해 내는 변수값이 미묘하게 서로 달라지는 게 보였다. 저 동일한 input 말고 계산에 영향을 끼치는 요소는 정말 없는데? 왜 값이 달라지지..?

그리고 결국은 설마 하던 녀석이 사람을 잡았다는 걸 알게 됐다. 문제의 함수는 바로.. 이것이었다!

unsigned long Rol(unsigned long x, long y)
{
    if (y % 32 == 0) {return x;}
    else {return ((x << y)^(x >> -y));}
}

저 간단한 함수의 실행 결과가 release 빌드와 debug 빌드가 서로 달랐다. 비주얼 C++ 2012, 2010, 2003 전부 공통으로.
암호화 알고리즘에서 절대 빠지지 않는 그 이름도 유명한 비트 회전(bit rotation)을 구현한 함수인데..
비트를 음수 shift하는 연산은 좀 생소해 보였다.

본인은 15년 가까이 C/C++ 프로그래밍을 해 오면서 지금까지 막연히 A<<-B = A>>B, A>>-B = A<<B이지 않으려나 생각해 왔다.
그런데 실상은 전혀 그렇지 않았다.
컴퓨터의 구조적인 특성상 나눗셈에서 피연산자의 부호에 음수가 섞이면 몫과 나머지의 부호가 수학에서 생각하는 직관적인 형태로 구해지지 않는다는 건 어렴풋이 알고 있었다만, 비트 shift에도 그런 특성이 있구나.

음수 shift의 결과는 언어 스펙 차원에서 undefined인 모양이다. 진짜 말 그대로 A=A++처럼 '그때 그때 달라요'인 듯.
중의적인 코드를 컴파일러마다 제멋대로 번역하는 것 자체를 모조리 막을 수는 없겠지만, 그건 최소한 '이식성'에 문제가 생길 수 있다고 경고라도 띄워야 하지 않나 싶다.

실제로 위의 함수를 실행하면

Rol(0xBE9F8300, 1);
Rol(0xEC6BFC33, 1);
Rol(0xFC58371A, 1);

의 함수값은 release 빌드에서는 각각 0x7D3F0600, 0xD8D7F866, 0xF8B06E34이 나온다.
그러나 debug 빌드에서는 0x7D3F0601, 0xD8D7F867, 0xF8B06E35가 나오며, 이게 맞는 값이다. release는 무슨 이유에서인지 최하 자리 1비트를 누락하고 있었던 것이다. 그러니 이후의 암호화 결과가 몽땅 틀어지는 건 당연지사.
설상가상으로 xcode에서는 더 이상한 결과가 나왔던 걸로 기억한다.

유명 암호화 라이브러리가 왜 저렇게 이식성 없는 연산을 썼는지 난 잘 모르겠다. 음수 shift의 결과가 어떻게 나올 것을 기대한 건지?
저 문제를 우회하느라 지금까지 머리로만 알고만 있었지 실무에서 쓸 일이 전혀 없으리라 생각했던 테크닉을 쓰게 됐다.
소스 코드의 특정 구간에 한하여 최적화를 잠시 끄는 #pragma optimize("", off) 되시겠다.

bit rotation은 bit shift에다가 한쪽 끝에 있는 비트들을 따로 반대편 끝에다 shift시켜서 얹어 준다는 차이만이 있을 뿐이다. 32비트 부호 없는 정수 기준으로, 작은 자리수가 큰 자리로 이동하는 왼쪽(<<) rotation을 나보고 구현하라면 이렇게 짜겠다.

UINT Rol2(UINT x, int y)
{
    return (x<<y)|(x>>(32-y));
}

32라는 숫자가 보기 싫으면 sizeof 등을 써서 다른 방식으로 바꾸면 되고.
그리고 이렇게만 짜도 컴파일러는 이 연산 전체의 의미를 알아보고 당연히 rol이라는 '비트 왼쪽 회전'이라는 '한 인스트럭션'으로 최적화해서 번역해 준다. bit shift인 shl, shr만큼이나 rotation도 굉장히 기계 친화적인 동작이며, 전용 명령이 있는 것이다. 하지만 정작 저 공개 라이브러리 함수는 Visual C++ 컴파일러가 rol이라고 최적화하지 않는다.

아마 -n shift는.. 전체 비트수에 대한 보수(32-n)만치 shift하는 것과 같다고 전제를 한 듯하다.
그리고 or 대신 xor을 쓴 것은 그게 컴퓨터 구조 차원에서 기계어 코드 길이가 더 짧거나 속도가 조금이라도 더 빨라서 그런 듯하다. 필요하다면 x=0조차도 x^=x로 표현하는 게 컴퓨터 세계이니 말이다.

결국은 음수 처리까지 정확하게 해서 shift든 rotation이든 -n만치 하는 건 반대편으로 n만치 하는 것과 같은 게 보장되는 함수를 만들려면..
if문을 써서 처리를 완전히 따로 하고 <<, >> 자체에는 어떤 경우든 음수 shift가 존재하지 않게 하는 게 이식성 면에서 가장 좋은 해결책으로 보인다. 흥미진진한 경험을 한 날이었다.

Posted by 사무엘

2014/06/15 08:36 2014/06/15 08:36
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/974

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

요즘 운영체제들에 GUI 셸이 없는 물건은 없고, GUI에는 그림보다도 먼저 문자를 찍는 기능이 반드시 필요하다. 옛날에는 그런 출력 기능이 겨우 비트맵 글꼴밖에 지원되지 않았지만, 오늘날은 트루타입(TTF)이라고 불리는 규격의 윤곽선 글꼴이 세계를 평정한 지 오래다(오픈타입은 TTF의 superset에 해당함). 심지어 재래식 비트맵 글꼴이 필요하다 해도 일단 TTF 방식으로 저장하고서 출력한다.

게임처럼 완전 독자적인 GUI 노선을 가는 프로그램이 아닌 이상, 거의 모든 응용 프로그램들은 운영체제가 제공하고 운영체제에 기본으로 설정되어 있는 글꼴만을 사용하여 글자를 출력한다. 새로운 글꼴을 받아서 설치하는 건 사용자의 몫이다. 그러나 가끔은 응용 프로그램이 직접 글꼴을 설치해서 써야 하는 경우도 있다.

워드 프로세서 같은 오피스 프로그램이라면 운영체제 전체에 새로운 글꼴을 번들로 제공할 수 있다. 이건 global한 글꼴 추가이다. 한편, 자기 프로그램 내부에서만 특수한 custom 글꼴을 추가해서 쓰는 건 local (private)한 글꼴 추가이다.

윤곽선 글꼴 출력 엔진은 힌팅과 캐싱 기능이 곁들여진 일종의 고성능 범용 단색 벡터 그래픽 엔진이다. 그렇기 때문에 다른 프로그램들에 노출되지 않는 local 글꼴도 용도가 매우 다양하다. 수식이나 악보에서 쓰이는 비문자 기호를 찍는 건 물론이고, ‘입꼴 워드’처럼 자기만 사용하는 특수한 문자를 찍을 때도 전용 글꼴을 활용하면 된다.

당장 운영체제 자신도 이걸 잘 활용하고 있다. 테마가 도입되기 전에 Windows 창에 달린 사각형 모양의 최소화(_)/최대화/닫기(X) 그림은 글꼴 출력이고, Visual Studio 같은 데서 창을 도킹시키는 주사기/핀 모양의 그림도 글꼴이다. 아마 본인이 옛날에 블로그 글에서 언급한 적이 있을 것이다.

Windows 8은 부팅 시나 작업 중일 때 다섯 개의 구슬이 동그란 궤도를 그리면서 슝슝 돌아가는 애니메이션이 출력되는데, 이것도 애니메이션 GIF나 플래시 같은 기술이 아니라 글꼴 출력이다~! 구슬이 싹 들어갔다가 나오기도 하는 게 은근히 복잡하며, 이 애니메이션은 무려 100수십 프레임에 달한다. 유니코드 PUA 영역에다 미리 계산된 각 프레임의 모양을 그려 넣은 뒤, 그 글자를 순서대로 찍은 것이다. 놀랍지 않은가?

보급이 아닌 싸제 글꼴의 등록 및 해제를 위해 Windows는 AddFontResource와 RemoveFontResource라는 간단한 함수를 제공한다. 인자로는 등록하거나 해제하고 싶은 글꼴 파일의 경로만 달랑 주면 된다. '일단은' 말이다. 그러다 나중에는--Windows 9x 라인 말고 2000에서부터-- 두 종류의 함수가 더 추가되었다.

첫째, 바로 저 두 함수의 이름 끝에다 Ex가 붙은 버전이다. Ex 버전은 인자를 두 개 더 받는데, 하나는 아직 reserved 상태니 별 의미가 없고, 다른 하나는 사소한 비트 플래그들이다. 등록하는 이 글꼴을 시스템 전체가 아니라 우리 프로세스 내부에서만 사용하게 하는 FR_PRIVATE 옵션, 그리고 글꼴의 접근 가능 여부를 떠나서 일단 이게 EnumFontFamilies(Ex)에서 집계가 되지 않게 하는 FR_NOT_ENUM 옵션이다. 즉, 이 글꼴의 독특한 이름을 아는 프로그램만 이 글꼴을 사용할 수 있게 되는 것이다.

그리고 둘째로, 글꼴을 파일 이름이 아니라 아예 메모리 상의 데이터로 받는 AddFontMemResourceEx도 추가되었다. 이 함수로 추가되는 글꼴은 파일로 실체가 존재하지도 않고 특정 프로세스의 주소 공간에 매여 있으므로 극도로 private하며, FR_PRIVATE|FR_NOT_ENUM 속성이 언제나 선택의 여지 없이 붙는다.

요컨대 글꼴을 좀 더 가볍게 private 형태로 추가하는 기능은 Windows 2000에 와서야 새로 도입된 셈이다. 여담이지만, 이것 말고도 Windows 2000은 9x/NT4 시절에 비해 프로그램의 국제화 수준이 크게 강화된 첫 버전인지라 다국어 IME와 complex script를 포함해 글꼴을 저수준에서 조작하는 API들도 크게 추가되었다.
트루타입 글꼴의 테이블 데이터를 있는 그대로 뽑아 내는 GetFontData라든가, 글꼴이 지원하는 문자 집합을 유니코드 번호로 얻어 오는 GetFontUnicodeRanges도 이때의 산물임.

뭐 그건 그렇고 다시 글꼴 등록 얘기로 돌아오자면..
local/private 말고 전통적인 global한 글꼴 추가도 여전히 필요한 절차이다.
그런데 문제는 이게 사실 함수 호출만 한다고 완전히 끝나는 게 아니라는 것이다. 절차가 생각보다 굉장히 지저분하며 문서화가 제대로 돼 있지 않다.

1. global 글꼴은 Windows\Fonts 디렉터리에 있어야 한다. 결국 파일을 복사해 넣어야 하는데, 이 디렉터리에 read가 아닌 write를 하려면 관리자 권한이 필요하다.

2. Fonts 디렉터리에 복사된 파일을 상대로 AddFontResource(Ex) 함수를 호출한다.

3. 이 글꼴이 다음 부팅 때에도 제대로 인식되게 하려면, 글꼴 리스트를 레지스트리에다가도 등록해 줘야 한다. 위치는 다음과 같다.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
과거 9x 시절에는 Windows NT 대신 그냥 Windows이고. 저 레지스트리도 read가 아닌 write를 하려면 관리자 권한이 필요하다.


레지스트리에 등록하는 형식은 대충 보면 짐작할 수 있지만, 문제는 이 뻔한 패턴의 작업을 자동으로 대행해 주는 함수가 없다는 것이다. 등록하고자 하는 TTF 파일을 직접 파싱해서 name 테이블에 있는 이름을 얻어 와야 하나? ActiveX 컨트롤을 등록해 주는 regsvr32 유틸리티처럼 글꼴을 명령 프롬프트에서 바로 설치하거나 제거하는 유틸리티도 운영체제에 있어야 할 것 같다.

옛날에는 트루타입 글꼴을 설치하려면 CreateScalableFontResource 같은 이상한 함수도 호출해서 ttf에 대응하는 *.fot 파일이라는 걸 만들어야 했던 모양이다. 완전 불편하기 그지없는데 지금은 그럴 필요까지는 없는 듯. 20년 전 엄청 옛날의 Windows 3.1 시절에는 ttf/fot 파일 쌍이 필요했지만 95 이후로는 그런 건 없다.

반대로 이 글꼴을 제거하려면 먼저 RemoveFontResource(Ex)를 호출해 주고, 이게 성공하면 레지스트리 제거와 파일 제거를 수행하면 된다.
그런데 Windows는 파일 자체를 가상 메모리 주소 공간에다 직통으로 대응해서 쓰는(MMF) 걸 좋아하는 운영체제인지라, 시스템 공용 파일을 지우기가 더럽게 까다로운 운영체제다. 글꼴도 예외가 아니어서 파일 삭제는 access deny 에러가 뜨면서 잘 되지 않을 수도 있다. 이때는 MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT)을 줘서 다음 재부팅 때라도 파일이 삭제되게 플래그를 주면 될 것이다.

local과는 달리 global 글꼴 등록과 삭제는 이렇게 번거로운데, 게다가 관리자 권한까지 필요하니 더욱 번거롭다.
관리자 권한은 한 프로세스가 필요한 때만 잠시 사용자의 동의 하에 취득했다가 반납하는 게 없다. 애초에 자기 프로그램을 더 높은 권한으로 재실행해야 한다.
잠시 다음 상황을 생각해 보자.

  •  어떤 일을 하는 동안에도 GUI는 매끄럽게 반응하고, 작업이 취소 가능하거나 진행 상황 같은 걸 별도로 표시해야 하는 경우: 작업 부분을 별도의 스레드로 떼어 내야 한다.
  • 다른 프로세스를 훅킹해서 정보를 얻어 오거나 실행을 조작해야 하는 경우: 훅 프로시저는 반드시 별도의 DLL로 만들어야 한다. DLL은 32비트와 64비트를 모두 신경 써서 만들어야 하니 더욱 번거롭다.

그리고,

  • 평소에는 일반 모드로 실행되지만, 잠시 관리자 권한을 얻어 와서 민감한 디렉터리나 레지스트리의 내용을 변경해야 하는 경우: 그 부분만 별도의 EXE(프로세스)로 만들어서 실행해야 한다. 물론 나 자신을 특수한 인자를 주고 재실행하는 것도 괜찮다.

참고로 권한이 낮은 프로그램은 권한이 높은 프로그램에다 메시지를 못 보낸다. 그러니 프로그램 간의 통신 메커니즘도 잘 생각해 봐야 한다. =_=;;

어지간하면 골치아플 일 없이 단일 모듈, 단일 스레드만으로 모든 일을 처리하고 싶은데 그럴 수가 없는 상황이 이렇게 요약되었다.
프로세스, 스레드, DLL이 시나리오별로 다 등장했다. 글꼴 설치는 '프로세스' 분리가 필요한 작업인 것이다.

Posted by 사무엘

2014/05/26 08:23 2014/05/26 08:23
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/967

« Previous : 1 : ... 13 : 14 : 15 : 16 : 17 : 18 : 19 : 20 : 21 : ... 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  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        

Site Stats

Total hits:
2673473
Today:
165
Yesterday:
1540