선택이란 작업 대상을 지정하는 행위이며 편집기능이 있는 대부분의 프로그램에서 공통적으로 지원하는 기능이다. 윈도우즈의 바탕 화면은 아이콘을 선택할 수 있도록 하며 탐색기는 파일을 선택한다. 그래픽 편집 프로그램이라면 원이나 사각형 같은 객체를 선택할 것이고 워드프로세서는 도표나 텍스트, 그림 따위를 선택하도록 해야 하는데 때로는 성질이 다른 객체들을 한꺼번에 선택할 경우도 있어 선택 자체가 무척 복잡하다.

텍스트 편집기의 경우 선택의 대상이 되는 것은 단순한 문자열이므로 그 범위만 제대로 관리하면 된다. 다른 프로그램에 비해 편집기는 선택을 관리하기가 쉽다. 선택된 문자열은 복사, 잘라내기, 삭제, 이동 등 여러 동작의 대상이 된다. 선택은 마우스로도 할 수 있고 키보드로도 할 수 있는데 우선 마우스 선택부터 해보도록 하자.

. 마우스로 이동하기

선택 기능 실습을 위해 먼저 ApiEdit4프로젝트를 새로 만든다. 그리고 OnCreate에 테스트 문장을 초기화해놓는다. 선택중의 스크롤 기능을 테스트해보기 위해 좀 긴 문자열이 필요하다. 다음 문장은 안빈낙도를 노래한 정극인의 상춘곡 일부이다. 참 좋은 문장인데 옛글 표기를 현대어로 바꾸다 보니 원문과는 좀 다르다.

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     lstrcpy(buf,"홍진에 묻친분네 이내 생애 엇더한고, 녯사람 풍류랄 미찰까 못미찰가. "

          "천지간 남자몸이 날만한이 하건마난 산림에 뭇혀 이셔 지락을 마랄것가. "

          "수간 모옥을 벽계수 앏픠두고 송죽 울울리예 풍월 주인 되여셔라.\r\n"

          "엇그제 겨을지나 새봄이 도라오니, 도화행화난 석양리예 퓌여있고 "

          "녹양방초난 세우중에 프르도다. 칼로 말아 낸가, 붓으로 그려 낸가. "

          "조화신공이 물물마다 헌사랍다.\r\n"

          "공명도 날 끠우고 부귀도 날 끠우니 청풍 명월외예 엇던 벗이 잇사올고,"

          "단표 누항에 흣튼 혜음 아니 하네. 아모타, 백년 행락이 이만한들 엇지하리");

 

ApiEdit3까지는 마우스 지원이 전혀 없었다. 마우스 메시지를 전혀 처리하지 않기 때문에 클릭해봐야 캐럿도 이동하지 않고 완전히 무반응이었다. 선택 루틴을 작성하기 전에 먼저 마우스를 클릭한 위치로 캐럿을 이동시키는 루틴부터 작성해보자.

이 작업을 하려면 클릭한 화면 좌표와 대응되는 메모리상의 오프셋을 구할 수 있는 방법이 있어야 한다. 마우스 메시지 핸들러에서 직접 구하기는 번거로우므로 이 작업을 전담하는 GetOffFromXY 함수를 여기서 작성할 것이다. 이로써 편집기 제작에 필수적인 GetAFromB 함수 4개가 일단 완성된다. 차후에 몇 가지 함수가 더 필요하지만 이 4가지 함수로 웬만큼의 기능은 다 만들 수 있다. 코드는 다음과 같다.

 

int GetOffFromXY(int x, int y)

{

     int r,s,e,len;

     HDC hdc;

     TCHAR *p;

     int chWidth;

     int acwidth;

 

     x=max(x,0);

     y=max(y,0);

 

     r=y/LineHeight;

     r=min(r,GetRowCount()-1);

 

     GetLine(r,s,e);

     hdc=GetDC(hWndMain);

 

     for (p=buf+s,acwidth=0;;) {

          if (p-buf == e) {

               break;

          }

 

          if (*p == ‘\t’) {

               len=1;

               chWidth=(acwidth/TabSize+1)*TabSize-acwidth;

          } else {

               if (IsDBCS(p-buf)) {

                   len=2;

               } else {

                   len=1;

               }

               chWidth=GetCharWidth(hdc,p,len);

          }

 

          acwidth += chWidth;

          if (acwidth-chWidth/2 >= x) {

               break;

          }

 

          p+=len;

     }

 

     ReleaseDC(hWndMain, hdc);

     if (p-buf==e && buf[p-buf]!=‘\r’ && buf[off]!=0) {

          bLineEnd=TRUE;

     } else {

          bLineEnd=FALSE;

     }

     return p-buf;

}

 

두 개의 인수 x, y를 받으며 이 위치와 대응되는 메모리상의 오프셋을 찾아 리턴한다. 이때 인수로 전달되는 x, y는 화면상의 좌표가 아니라 문서상의 좌표이다. 문서의 원점을 기준으로 한 좌표이므로 호출원에서는 반드시 스크롤 상태를 적용하여 문서 좌표를 전달해야 한다. 마우스로 작업영역을 클릭할 때 이 함수가 호출되는데 이때 x, y 좌표는 양수값이다. 하지만 마우스 버튼을 클릭한 채로 움직일 때는 커서가 작업영역 밖을 벗어날 수도 있으며 이때 x, y는 음수 좌표를 가질 수도 있다.

문서상의 모든 오프셋은 양수 좌표만을 가지므로 음수좌표와 대응되는 오프셋은 없다. 그래서 인수로 전달된 x, y는 반드시 0보다 큰 값을 가지도록 했다. 이렇게 되면 커서가 화면 위에 있을 때 첫 줄과 대응되는 오프셋을 찾아줄 것이고 왼쪽에 있으면 첫 글자의 오프셋을 찾아줄 것이다.

줄번호는 문서상의 y 좌표를 줄간으로 나누면 쉽게 구할 수 있다. , y가 아무리 커도 문서상의 끝 줄 아래가 될 수는 없으므로 줄번호의 상한값을 끝 줄로 만들었다. 작업영역의 아래 빈 영역을 클릭하면 수직으로 위에 있는 줄로 캐럿이 이동하는데 메모장으로 이런 동작을 테스트해보자.

GetLine으로 r 줄의 범위를 구하면 원하는 오프셋은 s~e사이에 있을 것이다. 이 범위에서 인수로 전달된 x와 대응되는 오프셋을 찾으면 된다. 비슷한 작업을 앞에서 이미 해 본적이 있는데 상하로 이동할 때 사용했던 GetXPosOnLine 함수를 호출하면 될 것 같다. 그러나 PrevX와 대응되는 오프셋을 찾는 문제와 클릭한 위치의 오프셋을 찾는 문제는 조금 달라 그렇게 할 수 없다. GetXPosOnLine PrevX이상의 오프셋을 찾지만 이 함수는 x와 가장 근접한 오프셋을 찾아야 한다.

줄의 처음부터 루프를 돌며 글자의 누적폭을 계산하는 것은 두 함수 모두 동일하다. 그러나 x 좌표와 누적폭을 비교하는 방식이 다르다. GetXPosOnLine은 누적폭이 x 이상인 오프셋을 찾지만 이 함수는 글자의 중앙 좌표와 x 좌표를 비교한다. x가 글자의 중앙을 넘어서는 최초의 오프셋을 찾아내는 것이다.

글자의 왼쪽을 누르면 왼쪽으로 캐럿이 가고 반대로 오른쪽을 누르면 다음 글자 위치로 이동한다. 만약 이렇게 하지 않고 누른 글자 위치로 가버리면 무척 불편할 것이다. 이 처리를 위해 마지막 읽은 글자의 폭은 따로 chWidth라는 변수에 저장해두었다가 누적폭에서 이 값의 절반을 빼 준 후 x와 비교하였다.

나머지 루틴은 GetXPosOnLine 함수와 거의 동일하며 bLineEnd에 대한 처리도 동일하다. 이 함수가 x 좌표에 대한 오프셋을 찾아 주므로 OnLButtonDown은 다음 두 줄로 간단하게 캐럿 위치를 옮길 수 있다.

 

void OnLButtonDown(HWND hWnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)

{

     off=GetOffFromXY(x+xPos,y+yPos);

     SetCaret();

}

 

마우스를 클릭한 화면상의 좌표는 x, y이고 이 좌표에 스크롤 상태를 적용하여 xPos, yPos를 더하면 문서상의 좌표가 된다. 이 좌표로부터 대응되는 오프셋을 찾아 전역변수off에 대입하고 SetCaret으로 캐럿의 위치를 옮겨주었다. 이제 실행해보면 마우스를 클릭한 곳으로 캐럿이 바로 이동할 것이다.