. ApiEditTest

윈도우 컨트롤로 만드는 것은 가능한 방법이기는 하지만 별로 좋은 선택이 아니다. 그래서 ApiEdit를 오브젝트로 다시 만든다. 그렇다고 해서 처음부터 코딩을 다시 할 필요는 없으며 대부분의 코드는 복사해서 붙여넣기만 하면 된다. 현재까지의 최종 버전인 ApiEdit9.cpp ShowMsg.cpp에 있는 도우미 클래스를 같이 합쳐서 ApiEdit 컨트롤의 소스를 재 작성해보자. 다음 실습 단계를 따라 하되 잔손이 많이 가기는 하지만 보기보다 시간이 많이 걸리지 않으므로 직접 해보기 바란다.

 

 ApiEditTest라는 이름으로 프로젝트를 생성한다. 이 프로젝트 안에 ApiEdit 컨트롤의 소스가 포함되는데 메인 윈도우는 ApiEdit를 테스트하기 위해 존재하므로 프로젝트 이름을 ApiEditTest로 결정했다. 이 프로젝트는 실제 사용을 목적으로 하는 응용 프로그램을 만드는 것이 아니라 ApiEdit 컨트롤의 여러 가지 기능을 테스트해보고 개발하기 위한 목적으로 작성하는 것이다.

새로운 Win32 응용 프로그램 프로젝트를 시작하되 부속파일없이 빈 프로젝트만 작성해놓는다. IME 함수를 호출하므로 미리 imm32.lib를 링크해놓고 64비트 이식성 점검 옵션은 선택하지 않는다. 비주얼 C++ 6.0으로 실습해도 상관없다.

 ApiEdit 컨트롤의 클래스를 정의하는 ApiEdit.h 헤더 파일을 추가하고 다음과 같이 코드를 작성한다. 직접 입력할 필요없이 ApiEdit9.cpp에서 복사해 붙이기만 하면 된다. 일부 추가된 멤버 외에는 모두 ApiEdit9.cpp에 있던 코드 그대로이다.

 

#ifndef __APIEDIT_H

#define __APIEDIT_H

 

#define IDM_AE_CUT 40001

#define IDM_AE_COPY 40002

#define IDM_AE_PASTE 40003

#define IDM_AE_SELALL 40004

#define MAXBOOKMARK 100

 

struct tagLine

{

     int Start;

     int End;

     int nPara;

     int nLine;

};

 

class CApiEdit

{

private:

     TCHAR *buf;

     BOOL bComp;

     int off;

     int FontHeight;

     int LineRatio;

     int LineHeight;

     int nWrap;

     BOOL bNoFirstSpace;

     int PrevX;

     BOOL bLineEnd;

     int yPos,xPos;

     int yMax,xMax;

     int FontWidth;

     int TabWidth;

     int TabSize;

     int SelStart, SelEnd;

     BOOL bCapture;

     COLORREF SelFore, SelBack;

     int HideSelType;

     int arChWidth[128];

     BYTE *arHanWidth;

     tagLine *pLine;

     int TotalLine;

     HBITMAP hBit;

     int MarginWidth;

     COLORREF MarColor1, MarColor2;

     RECT frt;

     BOOL bShowLineNum;

     HFONT hLineNumFont;

     COLORREF NumColor;

     int arMark[MAXBOOKMARK];

     COLORREF MarkColor;

     BOOL bShowTab;

     BOOL bShowEnter;

     BOOL bShowSpace;

     int ShowTabType;

     int ShowEnterType;

     int ShowSpaceType;

     COLORREF CodeColor;

     int nShowCurLine;

     COLORREF CurColor;

     BOOL bSelLine;

     int SelStartLine;

     HCURSOR hCSel,hCCopy,hCMove,hCMargin,hCNoDrop;

     BOOL bDragSel;

     BOOL bReadOnly;

     BOOL bOvr;

     int buflen;

     int Linelen;

     int doclen;

 

public:

    HWND hWnd;

    CApiEdit();

    ~CApiEdit();

 

    BOOL Create(int x,int y,int w,int h,DWORD style,UINT id,HWND hParent);

    LRESULT OnMessage(UINT iMessage,WPARAM wParam,LPARAM lParam);

 

     BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct);

     void OnChar(HWND hWnd, TCHAR ch, int cRepeat);

     void OnKey(HWND hWnd, UINT vk, BOOL fDown, int cRepeat, UINT flags);

     void OnPaint(HWND hWnd);

     void OnSetFocus(HWND hWnd, HWND hwndOldFocus);

     void OnKillFocus(HWND hWnd, HWND hwndNewFocus);

     void OnDestroy(HWND hWnd);

     LRESULT OnImeComposition(HWND hWnd, WPARAM wParam, LPARAM lParam);

     LRESULT OnImeChar(HWND hWnd, WPARAM wParam, LPARAM lParam);

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

     void OnMouseMove(HWND hWnd, int x, int y, UINT keyFlags);

     void OnLButtonUp(HWND hWnd, int x, int y, UINT keyFlags);

     void OnTimer(HWND hWnd, UINT id);

     void OnSize(HWND hWnd, UINT state, int cx, int cy);

     void OnHScroll(HWND hWnd, HWND hwndCtl, UINT code, int pos);

     void OnVScroll(HWND hWnd, HWND hwndCtl, UINT code, int pos);

     void OnContextMenu(HWND hWnd, HWND hwndContext, UINT xPos, UINT yPos);

     void OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify);

     BOOL OnSetCursor(HWND hWnd, HWND hwndCursor, UINT codeHitTest, UINT msg);

     UINT OnGetDlgCode(HWND hWnd, LPMSG lpmsg);

     void OnMouseWheel(HWND hWnd, WPARAM wParam, LPARAM lParam);

 

     int GetCharWidth(TCHAR *ch, int len);

     void SetCaret(BOOL bUpdatePrevX=TRUE, BOOL bScrollToCaret=TRUE);

     inline BOOL IsDBCS(int nPos);

     int GetPrevOff(int nPos);

     int GetNextOff(int nPos);

     void Insert(int nPos, TCHAR *str);

     void Delete(int nPos, int nCount);

     void MySetImeMode(HWND hEdit, BOOL bHan);

     void GetLine(int Line, int &s, int &e);

     int GetRowCount();

     void GetRCFromOff(int nPos, int &r, int &c);

     int GetOffFromRC(int r, int c);

     void GetXYFromOff(int nPos, int &x, int &y);

     void SetWrap(int aWrap);

     int GetXPosOnLine(int r,int DestX);

     void UpdateScrollInfo();

     int DrawLine(HDC hdc, int Line);

     void DrawSegment(HDC hdc, int &x, int y, int SegOff, int len, BOOL ignoreX,

          COLORREF fore, COLORREF back);

     int GetOffFromXY(int x, int y);

     BOOL IsDelimiter(int nPos);

     int GetPrevWord(int nPos);

     int GetNextWord(int nPos);

     void ClearSelection();

     void ExpandSelection(int Start, int End);

     BOOL DeleteSelection();

     void PrepareCharWidth(HDC hdc);

     int MyGetTextExtent(TCHAR *text, int len);

     void UpdateLineInfo(int nPos=-1, int nCount=-1);

     void Invalidate(int Pos1, int Pos2=-1);

     int FindParaStart(int nPos);

     void ClearBookmark();

     void ToggleBookmark(int Para, int Mark);

     void GotoBookmark(int Mark);

     void GotoLine(int Line);

     int FindBookmark(int Para);

     void DisplayTab(HDC hdc, int x1, int x2, int y,COLORREF back);

     void DisplayEnter(HDC hdc, int x, int y,COLORREF back);

     void DisplaySpace(HDC hdc, int x, int y, TCHAR *text, int len);

     void GetNowWord(int nPos, int &s, int &e);

     int IncludeEnter(int nPos);

     BOOL IsInSelection(int x,int y);

     void CopyString(BOOL bCopy, int from, int &to, int len);

};

 

#endif // __APIEDIT_H

 

팝업메뉴의 ID, MAXBOOKMARK 매크로 상수와 tagLine 구조체의 정의를 복사해 오고 CApiEdit 클래스를 정의하였다. 전역변수들은 모두 이 클래스의 private 멤버가 되었으며 모든 함수들은 이 클래스의 멤버함수가 된다. 변수 선언문과 함수 원형 선언을 그대로 복사해 넣기만 하면 된다. 단 전역변수 중에 g_hInst hWndMain 그리고 lpszClass는 컨트롤과 관련된 변수가 아니므로 복사해올 필요없다. WndProc 함수도 도우미의 ApiEditProc으로 대체되었으므로 삭제하도록 한다.

새로 추가된 멤버는 윈도우 핸들을 저장할 hWnd와 생성자, 파괴자 그리고 Create 함수와 메시지를 처리할 OnMessage 함수 정도뿐이다. 헤더 파일이 여러 번 중복 포함될 때를 대비해서 __APIEDIT_H 매크로를 정의하였다. 헤더 파일에 이런 조건식을 두는 것은 일반적인데 앞으로 만들 헤더 파일에도 모두 중복 포함을 방지하는 이런 조건부 컴파일 지시자를 사용할 것이다.

 ApiEdit.cpp 파일을 추가하고 도우미 클래스를 작성한다. 이미 앞에서 만들어 두었으므로 ShowMsg.cpp에서 그대로 복사해 오면 된다.

 

#define _WIN32_WINNT 0x400

#define _WIN32_WINDOWS 0x401

#include <windows.h>

#include <windowsx.h>

#include <imm.h>

#include <stdio.h>

#include "ApiEdit.h"

 

LRESULT CALLBACK ApiEditProc(HWND,UINT,WPARAM,LPARAM);

 

class CRegisterHelper

{

public:

     CRegisterHelper();

     ~CRegisterHelper();

 

     struct _arObj

     {

          CApiEdit *pObj;

          HWND hWnd;

     } *arObj;

     int arSize;

     int nReg;

 

     CApiEdit *FindObject(HWND hWnd);

     void AddObject(HWND hWnd, CApiEdit *pObj);

     void RemoveObject(HWND hWnd);

};

 

CRegisterHelper::CRegisterHelper()

{

     WNDCLASS WndClass;

 

     WndClass.cbClsExtra=0;

     WndClass.cbWndExtra=0;

     WndClass.hbrBackground=NULL;

     WndClass.hCursor=LoadCursor(NULL,IDC_IBEAM);

     WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

     WndClass.hInstance=GetModuleHandle(NULL);

     WndClass.lpfnWndProc=(WNDPROC)ApiEditProc;

     WndClass.lpszClassName="ApiEdit";

     WndClass.lpszMenuName=NULL;

     WndClass.style=CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

     RegisterClass(&WndClass);

 

     nReg=0;

     arSize=10;

     arObj=(_arObj *)malloc(arSize*sizeof(_arObj));

     memset(arObj,0,arSize*sizeof(_arObj));

}

 

CRegisterHelper::~CRegisterHelper()

{

     free(arObj);

     arObj=NULL;

}

 

CApiEdit *CRegisterHelper::FindObject(HWND hWnd)

{

     int i;

 

     for (i=0;i<nReg;i++) {

          if (arObj[i].hWnd == hWnd)

              return arObj[i].pObj;

     }

     return NULL;

}

 

void CRegisterHelper::AddObject(HWND hWnd, CApiEdit *pObj)

{

     int i;

 

     if (nReg == arSize-1) {

          arSize++;

          arObj=(_arObj *)realloc(arObj,arSize*sizeof(_arObj));

          arObj[arSize-1].hWnd=NULL;

          arObj[arSize-1].pObj=NULL;

     }

 

     for (i=0;;i++) {

          if (arObj[i].hWnd == NULL)

              break;

     }

     arObj[i].hWnd=hWnd;

     arObj[i].pObj=pObj;

     pObj->hWnd=hWnd;

     nReg++;

}

 

void CRegisterHelper::RemoveObject(HWND hWnd)

{

     int i,j;

 

     if (IsWindow(hWnd)) {

          DestroyWindow(hWnd);

     }

     for (i=0;i<nReg;i++) {

          if (arObj[i].hWnd == hWnd)

              break;

     }

     for (j=i+1;j<arSize;j++) {

          arObj[j-1].hWnd=arObj[j].hWnd;

          arObj[j-1].pObj=arObj[j].pObj;

     }

     nReg--;

}

 

CRegisterHelper _RegisterHelper;

 

LRESULT CALLBACK ApiEditProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     CApiEdit *pAE;

 

     pAE=_RegisterHelper.FindObject(hWnd);

     if (pAE == NULL) {

          pAE=(CApiEdit *)((LPCREATESTRUCT)lParam)->lpCreateParams;

          _RegisterHelper.AddObject(hWnd,pAE);

     }

 

     return pAE->OnMessage(iMessage, wParam, lParam);

}

 

메시지 크래커를 사용하므로 windowsx.h가 필요하고 그 외 imm.h stdio.h 헤더 파일을 추가로 더 인클루드해야 한다. 도우미 클래스의 코드는 전혀 변경되지 않았다. 다만 CShowMsg 클래스가 CApiEdit로 바뀌었고 ShowMsgProc 함수가 ApiEditProc으로 대체되었을 뿐이다. 생성자에서 정의하는 윈도우 클래스는 ApiEdit.cpp WinMain에 있던 코드를 가져 왔다. 배경 브러시는 없고 커서는 IDC_IBEAM을 사용하고 더블클릭을 입력받아야 하므로 윈도우 클래스의 스타일에 CS_DBLCLKS 플래그를 추가하였다.

 생성자와 파괴자, Create 함수를 작성한다. 이 함수들은 ShowMsg.cpp에서 복사한 후 CreateWindow의 윈도우 클래스명만 ApiEdit로 바꾸면 된다.

 

CApiEdit::CApiEdit()

{

}

 

CApiEdit::~CApiEdit()

{

     if (_RegisterHelper.arObj)

          _RegisterHelper.RemoveObject(hWnd);

}

 

CApiEdit::Create(int x,int y,int w,int h,DWORD style,UINT id,HWND hParent)

{

     CreateWindow("ApiEdit",NULL, style,

          x,y,w,h,hParent,(HMENU)id,GetModuleHandle(NULL),this);

     return TRUE;

}

 

OnMessage 멤버함수의 코드는 ApiEdit9.cpp WndProc을 그대로 복사해 오면 된다. CALLBACK 지정자를 삭제하고 첫 번째 인수 hWnd도 삭제하도록 하자. OnMessage는 멤버함수이므로 hWnd 멤버변수로부터 윈도우 핸들을 구할 수 있다.

 

LRESULT CApiEdit::OnMessage(UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     switch(iMessage) {

          HANDLE_MSG(hWnd, WM_CREATE, OnCreate);

          HANDLE_MSG(hWnd, WM_DESTROY, OnDestroy);

          HANDLE_MSG(hWnd, WM_CHAR, OnChar);

          HANDLE_MSG(hWnd, WM_KEYDOWN, OnKey);

          HANDLE_MSG(hWnd, WM_KEYUP, OnKey);

          HANDLE_MSG(hWnd, WM_PAINT, OnPaint);

          HANDLE_MSG(hWnd, WM_SETFOCUS, OnSetFocus);

          HANDLE_MSG(hWnd, WM_KILLFOCUS, OnKillFocus);

          HANDLE_MSG(hWnd, WM_LBUTTONDOWN, OnLButtonDown);

          HANDLE_MSG(hWnd, WM_LBUTTONDBLCLK, OnLButtonDown);

          HANDLE_MSG(hWnd, WM_MOUSEMOVE, OnMouseMove);

          HANDLE_MSG(hWnd, WM_LBUTTONUP, OnLButtonUp);

          HANDLE_MSG(hWnd, WM_TIMER, OnTimer);

          HANDLE_MSG(hWnd, WM_SIZE, OnSize);

          HANDLE_MSG(hWnd, WM_HSCROLL, OnHScroll);

          HANDLE_MSG(hWnd, WM_VSCROLL, OnVScroll);

          HANDLE_MSG(hWnd, WM_CONTEXTMENU, OnContextMenu);

          HANDLE_MSG(hWnd, WM_COMMAND, OnCommand);

          HANDLE_MSG(hWnd, WM_SETCURSOR, OnSetCursor);

          HANDLE_MSG(hWnd, WM_GETDLGCODE, OnGetDlgCode);

     case WM_IME_COMPOSITION:

          return OnImeComposition(hWnd, wParam, lParam);

     case WM_IME_CHAR:

          return OnImeChar(hWnd, wParam, lParam);

     case WM_IME_STARTCOMPOSITION:

          return 0;

     case WM_MOUSEWHEEL:

          OnMouseWheel(hWnd,wParam,lParam);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

 ApiEdit9.cpp에 있는 모든 함수들의 코드를 몽땅 복사해 ApiEdit.cpp에 붙인다. OnCreate이하 모든 함수를 그대로 가져 오면 된다. 코드는 전혀 손댈 것이 없다. 모든 함수들이 CApiEdit 클래스의 멤버함수가 되었으므로 함수이름 앞에 CApiEdit::만 붙여주면 된다. 대표적으로 몇 가지 함수만 보인다.

 

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

{

....

 

void CApiEdit::OnDestroy(HWND hWnd)

{

....

 

void CApiEdit::OnSize(HWND hWnd, UINT state, int cx, int cy)

{

....

 

비주얼 C++ 7.0은 편집기의 팝업메뉴에서 개요/정의 부분만 보이기 항목을 선택하면 본체 코드는 축소되고 함수의 정의부만 보여주므로 이 상태에서 함수이름 앞에 CApiEdit::을 반복적으로 붙여넣기만 하면 된다.

그 동안 테스트를 위해 작성했던 임시코드도 남아 있다면 삭제하도록 한다. 속도 측정을 위해 임시로 작성해놓은 STARTQ, ENDQ 등의 매크로를 삭제하고 VK_F8, VK_F9도 필요가 없으므로 삭제한다.

 모든 코드에서 hWndMain을 제거한다. 이제 ApiEdit는 더 이상 메인 윈도우가 아니므로 자신의 핸들값인 hWnd 멤버변수를 대신 사용해야 한다. 일괄 치환 기능을 사용하여 hWndMain hWnd로 모두 바꿔 주고 OnCreate에 있는 hWndMain=hWnd 대입문은 불필요하므로 삭제한다. 여기까지 작업하면 ApiEdit 컨트롤은 완성된다.

 테스트를 위한 메인 윈도우를 작성한다. ApiEditTest.cpp 파일을 추가하고 다음과 같이 소스를 작성한다.

 

#include <windows.h>

#include "ApiEdit.h"

 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

HINSTANCE g_hInst;

HWND hWndMain;

LPCTSTR lpszClass=TEXT("ApiEditTest");

CApiEdit Ae;

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

       ,LPSTR lpszCmdParam,int nCmdShow)

{

     HWND hWnd;

     MSG Message;

     WNDCLASS WndClass;

     g_hInst=hInstance;

    

     WndClass.cbClsExtra=0;

     WndClass.cbWndExtra=0;

     WndClass.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);

     WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

     WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

     WndClass.hInstance=hInstance;

     WndClass.lpfnWndProc=(WNDPROC)WndProc;

     WndClass.lpszClassName=lpszClass;

     WndClass.lpszMenuName=NULL;

     WndClass.style=CS_HREDRAW | CS_VREDRAW;

     RegisterClass(&WndClass);

 

     hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,

          CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

          NULL,(HMENU)NULL,hInstance,NULL);

     ShowWindow(hWnd,nCmdShow);

    

     while(GetMessage(&Message,0,0,0)) {

          TranslateMessage(&Message);

          DispatchMessage(&Message);

     }

     return (int)Message.wParam;

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     switch(iMessage) {

     case WM_CREATE:

          hWndMain=hWnd;

          Ae.Create(0,0,0,0,WS_CHILD | WS_VISIBLE,1,hWnd);

          return 0;

     case WM_SIZE:

          MoveWindow(Ae.hWnd,0,0,LOWORD(lParam),HIWORD(lParam),TRUE);

          return 0;

     case WM_SETFOCUS:

          SetFocus(Ae.hWnd);

          return 0;

     case WM_DESTROY:

          PostQuitMessage(0);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

오브젝트화 실습에서 작성했던 ShowMsgTest.cpp와 별반 다를 것이 없다. ApiEdit 객체 Ae를 선언하고 WM_CREATE에서 Create 함수로 윈도우를 생성했다. WM_SIZE에서 이 컨트롤이 항상 작업영역을 가득 채우도록 했으며 WM_SETFOCUS에서 메인 윈도우가 포커스를 받으면 ApiEdit로 포커스를 넘기도록 하였다.

메인 윈도우의 작업영역 전체가 ApiEdit 컨트롤로 채워져 있으므로 작업영역에 아무것도 출력할 필요가 없으며 따라서 WM_PAINT 메시지도 처리하지 않았다. 윈도우 스타일에 CS_CLIPCHILDREN 플래그를 추가하여 차일드 영역은 그리기를 금지하였다.

 

실습 과정이 굉장히 길어 보이고 복잡해보이지만 새로 작성한 코드는 단 하나도 없다. 이미 앞에서 다 작성해 왔던 것들이고 오브젝트화 실습에서 테스트를 마친 코드들을 조합하여 프로젝트의 형태가 조금 바뀌었을 뿐이다. CRegisterHelper 도우미 클래스는 다른 컨트롤에도 얼마든지 재사용할 수 있도록 일반적으로 작성되어 있으므로 필요할 때 언제든지 복사해 쓸 수 있다. 클래스 템플릿을 사용하여 객체 맵의 타입을 지정할 수 있도록 정리해놓는다면 훨씬 더 쉽게 재활용할 수 있을 것 같다.