. 글꼴 변경

글꼴은 사용자의 눈에 직접 보이는 아주 중요한 요소이다. 어떤 글꼴로 텍스트를 읽느냐에 따라 문서의 가독성에 확연한 차이가 발생하므로 사용자들이 원하는 폰트를 쓸 수 있도록 해야 한다. 글꼴 변경 기능은 비단 편집기뿐만 아니라 글꼴을 조금이라도 사용하는 모든 프로그램이 갖추어야 할 필수 기능 중의 필수 기능이다. 글꼴을 바꿀 수 없다면 항상 제공하는 글꼴만 사용해야 하므로 이만 저만 불편한 것이 아니다.

ApiEdit는 아직까지 글꼴 변경에 대한 어떠한 지원도 없으며 무조건 시스템 폰트로만 문서를 보여주도록 되어 있다. 글꼴을 다른 것으로 바꾸는 기능을 넣기 위해 사용하는 글꼴에 대한 정보를 가지도록 만들어보자. 글꼴의 정보를 저장할 멤버변수를 다음과 같이 추가하고 폰트 조사 및 변경 함수도 추가한다.

 

class CApiEdit

{

     ....

     LOGFONT logfont;

     ....

public:

     ....

     void GetFont(LOGFONT *alogfont);

     void SetFont(LOGFONT *alogfont);

 

글꼴의 정보는 LOGFONT 구조체로 표현되며 이 구조체가 있으면 CreateFontIndirect 함수로 원하는 글꼴을 만들 수 있다. 덩치 큰 구조체를 저장하는 대신 폰트의 핸들을 대신 저장할 수도 있는데 이렇게 되면 ApiEdit는 핸들만으로 폰트를 쉽게 사용할 수 있는 대신 폰트를 만드는 책임이 호스트에게 전가된다는 부담이 있다. 더구나 호스트는 ApiEdit에게 제공한 폰트 핸들을 반드시 해제하는 것을 잊어서는 안되므로 리소스 누출의 위험까지 있다.

대부분의 표준 컨트롤은 폰트의 핸들을 전달함으로써 부모가 제공한 폰트를 사용하도록 되어 있는데 ApiEdit는 좀 다른 방법으로 작성하기로 하였다. 이 결정이 옳은지는 정확하게 판단하기 어렵지만 최대한 호스트에게 편의를 제공한다는 점에서는 괜찮은 결정인 것 같다. 이 구조체의 정보를 사용하는 주체는 OnPaint 함수이다.

 

void CApiEdit::OnPaint(HWND hWnd)

{

     ....

    HFONT hFont, hOldFont;

 

     hdc=BeginPaint(hWnd,&ps);

 

     t=yPos/LineHeight;

     s=(yPos+ps.rcPaint.top)/LineHeight;

     e=(yPos+ps.rcPaint.bottom-1)/LineHeight;

     e=min(e,TotalLine-1);

 

     hMemDC=CreateCompatibleDC(hdc);

     if (hBit == NULL) {

          hBit=CreateCompatibleBitmap(hdc,frt.right,LineHeight);

     }

     OldBitmap=(HBITMAP)SelectObject(hMemDC,hBit);

 

    if (logfont.lfHeight != 0) {

        hFont=CreateFontIndirect(&logfont);

        hOldFont=(HFONT)SelectObject(hMemDC,hFont);

    }

 

     hBrush=GetSysColorBrush(COLOR_WINDOW);

     SetRect(&lrt,MarginWidth-1,0,frt.right,LineHeight);

 

     ....

     SetRect(&lrt,MarginWidth-1,(l-t)*LineHeight,frt.right,frt.bottom);

     FillRect(hdc,&lrt,hBrush);

 

    if (logfont.lfHeight != 0) {

        DeleteObject(SelectObject(hMemDC,hOldFont));

    }

     ....

 

글꼴은 결국 DC에 선택되는 것이며 DC에 한 번 선택되면 이 DC를 사용하는 모든 함수들이 변경된 글꼴의 영향을 받게 된다. 그래서 OnPaint에서 글꼴을 만들어 DC에 한 번 선택해놓기만 하면 DrawLine, DrawSegment, ShowSpace 같은 졸병 함수들도 DC에 선택된 글꼴을 사용하게 되므로 OnPaint가 글꼴을 변경할 가장 최적의 장소이다.

logfont 구조체는 사용할 글꼴의 정보를 가지는데 아주 특수한 예외 값을 하나 가지고 있다. 폰트의 높이 즉, lfHeight 0이면 시스템 폰트를 사용하라는 뜻으로 해석하여 폰트 생성이나 선택 동작을 하지 않는다. logfont를 시스템 폰트의 정보로 초기화 해도 동일한 효과가 발생하지만 시스템 폰트는 굳이 만들 필요없이 DC에 디폴트로 선택되어 있으므로 아무 처리도 하지 않는 것이 실행속도를 향상시키는 방법이다. 그래서 logfont.lfHeight에 특별한 의미를 부여하였으며 이 약속을 폰트 관련 함수들은 모두 준수한다.

OnPaint logfont의 정보대로 글꼴을 생성하고 사용하도록 수정되었으므로 이제 호스트의 요구가 있을 때 logfont에 원하는 글꼴의 정보를 대입하기만 하면 된다. 설정된 폰트를 조사 및 변경하는 다음 두 함수를 ApiEdit.cpp에 작성한다. GetFont 함수는 글꼴의 정보를 조사하기만 하므로 별로 하는 일이 없지만 SetFont는 굉장히 복잡하다.

 

void CApiEdit::GetFont(LOGFONT *alogfont)

{

     *alogfont=logfont;

}

 

void CApiEdit::SetFont(LOGFONT *alogfont)

{

    HDC hdc;

    TEXTMETRIC tm;

     HFONT hFont, hOldFont=NULL;

 

     logfont=*alogfont;

    hdc=GetDC(hWnd);

     if (logfont.lfHeight != 0) {

          hFont=CreateFontIndirect(&logfont);

          hOldFont=(HFONT)SelectObject(hdc,hFont);

     }

    PrepareCharWidth(hdc);

    GetTextMetrics(hdc,&tm);

    FontHeight=tm.tmHeight;

    LineHeight=int(FontHeight*LineRatio/100);

    yPos=yPos-(yPos % LineHeight);

    FontWidth=tm.tmAveCharWidth;

    TabSize=FontWidth*TabWidth;

     if (hOldFont) {

          DeleteObject(SelectObject(hdc,hOldFont));

     }

    ReleaseDC(hWnd,hdc);

 

     if (hBit) {

          DeleteObject(hBit);

          hBit=NULL;

     }

 

     if (buf) {

          UpdateLineInfo();

          UpdateScrollInfo();

          SetCaret();

          Invalidate(-1);

     }

}

 

글꼴이 변경되면 영향을 받는 다른 정보들이 많이 있는데 예를 들어 줄간도 바뀌고 탭 크기도 바뀐다. 뿐만 아니라 정렬루틴에서 참조하는 문자의 폭이 모두 바뀌므로 PrepareCharWidth 함수도 다시 호출되어야 하며 더블 버퍼링 비트맵의 높이도 바뀌어야 한다. 이런 정보들이 바뀌면 정렬 상태도 영향을 받으므로 정렬도 다시 해야 하고 스크롤 정보도 재설정할 필요가 있다.

글꼴이 바뀌면 그야말로 ApiEdit의 내부 변수들이 왕창 바뀌게 되는 것이다. OnCreate에 있는 많은 초기화 코드 중 글꼴과 조금이라도 관련이 있는 코드는 모두 SetFont 함수로 이동되었으며 문서가 처음 만들어질 때를 제외하고는 정렬도 다시 해야 한다. 화면을 다시 그리고 캐럿의 위치도 재조정해야 하는 것은 물론이다. 그래서 SetFont 함수가 하는 일이 저렇게 많을 수밖에 없다.

줄간을 변경하는 SetLineRatio 함수에서와 마찬가지로 글꼴이 변경될 때도 불일치가 발생할 수 있다. 그래서 이 함수도 현재 위치는 유지해야 하며 화면에 보이도록 하고 yPos는 반드시 줄간의 배수가 되도록 하는 몇 가지 처리를 하고 있다. 그러나 첫 줄을 유지하는 처리는 하지 않는데 왜냐하면 글꼴이 바뀌면 이전 줄 정보 자체가 무효해지기 때문에 첫 줄을 유지할 수가 없기 때문이다.

불쌍한 OnCreate는 생성자와 InitDoc에게 대부분의 코드를 뺏기더니 이번에는 SetFont한테도 꽤 많은 양의 코드를 양보했다. 남은 코드는 문서나 글꼴과 상관이 없는 전역적인 초기화뿐이며 다음 코드만 남았다. 다행히 추가된 코드도 두 줄 있다.

 

BOOL CApiEdit::OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     MySetImeMode(hWnd,TRUE);

     bComp=FALSE;

     LineRatio=120;

     nWrap=2;

     bNoFirstSpace=FALSE;

     TabWidth=4;

     bCapture=FALSE;

     HideSelType=1;

     MarginWidth=25;

     MarColor1=RGB(192,192,192);

     MarColor2=RGB(160,160,160);

     bShowLineNum=FALSE;

     NumColor=RGB(0,0,0);

     MarkColor=RGB(255,255,0);

     bShowTab=FALSE;

     bShowEnter=FALSE;

     bShowSpace=FALSE;

     ShowTabType=1;

     ShowEnterType=1;

     ShowSpaceType=0;

     CodeColor=RGB(128,128,128);

     nShowCurLine=0;

     CurColor=RGB(255,255,0);

     bSelLine=FALSE;

     SelStartLine=0;

     bDragSel=FALSE;

     bWantTab=TRUE;

     SumDelta=0;

     bHideCaret=FALSE;

     InitDoc();

    logfont.lfHeight=0;

    SetFont(&logfont);

 

     return TRUE;

}

 

최초 ApiEdit가 만들어질 때는 시스템 폰트를 사용해야 하므로 logfont.lfHeight 0을 대입하였다. 그리고 새로 졸병 함수가 된 SetFont를 호출하여 글꼴과 관련된 초기화를 하도록 했다. 이제 호스트에서 글꼴을 바꿀 필요가 있을 때 LOGFONT 구조체를 만든 후 SetFont 함수만 불러 주면 된다.