안녕하십니까 이번 포스팅에서는 메모장 소스 코드를 설명드리겠습니다. 여태 포스팅에서 설명했던 부분은 넘어가겠습니다.

- #include <commctrl.h>

메세지박스를 호출하는 함수 TaskDialogIndirect를 사용하기 위해 선언하였습니다.

- #pragma comment(lib, "comctl32.lib")

comctl32.lib를 링크시키는 코드인데 이 pragma라고 하는 것은 컴파일러의 기능을 확장시킬 수 있도록 하는 지시어를 의미합니다. 이 지시어의 종류를 MSDN에서 한번 살펴보자면 

이정도가 있겠네요. 이 중 우리가 사용한 지시어는 comment입니다. comment를 한번 살 펴 봅시다.

이 comment는 type와 commentstring으로 이루어져 있는데 여기서는 type을 lib로 지정하였습니다. type이 lib라는 것은 지정한 라이브러리를 링크할 목적으로 쓰이며 이때 commentstring은 링크할 라이브러리명을 적어줍시다.

즉 이코드는 comctl32.lib 파일을 링크해주는 부분입니다. 그렇다면 왜 comctl32.lib파일을 링크해주는 것일까요?

기본적으로 exe파일이 컴파일러가 소스코드를 컴파일하면 나오는 결과물인 obj파일과 라이브러리 파일을 링킹한 결과물인 것은 다 아실 겁니다. 물론 visual studio에서는 이러한 작업을 자동으로 해줍니다. (이 과정을 빌드라고 합니다.) 그런데 이 visual studio에서 링킹 작업을 할 때 링크된 라이브러리를 이용해 작업을 하는데 이 링크된 라이브러리에 comctl32.lib가 자동으로 포함되어 있지 않기 때문입니다.

좀더 자세히 살펴보기 위해 프로젝트 - 속성 - 링커 - 입력 - 추가종속성 부분을 확인해 보시면

kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;
oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib

의 라이브러리 파일만 링크되어 있음을 알 수 있습니다. 그러므로 우리는 comctl32.lib 파일또한 링크하기 위해 이 코드를 작성합니다. 물론 이 코드를 작성하지 않고 직접 이 추가종속성 부분에 추가해주셔도 됩니다.

- LRESULT WINAPI WndProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam);

WndProc함수의 선언부부입니다.

- BOOL WINAPI PageDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam);

PageDlgProc 함수의 선언부분입니다. 이 함수는 DialogBox함수를 사용하여 우리가 작성한 리소스인 대화상자를 호출할 때 동작하는 대화상자 프로시저라고 보시면 됩니다.

- void SaveFile_as(HWND hWnd);
  void SaveFile(HWND hWnd);
  void FileOpen(HWND hWnd);
  void SaveDlgBox(HWND hWnd);

우리가 앞 포스팅에서 설명한 함수들을 선언하였습니다.

- HWND Edit;

Edit윈도우의 핸들을 저장하기 위해 선언하였습니다.

- HINSTANCE Global_hInstance;

WinMain의 hInstance를 다른 함수에서도 쓰기 위해 전역변수로 선언하였습니다.

- TCHAR FILEPATH[500]=L"제목 없음 - 메모장";

메모장의 제목표시줄 부분의 값을 저장하는 FILEPATH 전역변수를 선언합니다.

- int str_CHANGE = 0;
  int CANCEL = 0;

문자열 변경 여부를 저장하는 변수 str_CHANGE와 취소 버튼을 누른지의 여부를 저장하는 변수 CANCEL을 전역변수로 선언합니다.

WinMain 부분은 설명하지 않은 부분만 설명하겠습니다.

- HACCEL haccel;

LoadAccelerators함수의 결과값을 저장하기 위해 선언하였습니다. 즉 이 변수는 액셀러레이터의 핸들을 저장하는 변수이지요. 이 부분은 나중에 추가로 설명드리겠습니다.

- Global_hInstance = hInstance;

hInstance를 다른 함수에서도 사용하기 위해 전역변수에 값을 넣어줍니다.

- hwnd = CreateWindow(L"notepad",L"제목 없음 - 메모장",WS_OVERLAPPEDWINDOW | WS_VISIBLE,0,100,CW_USEDEFAULT,CW_USEDEFAULT,NULL,
   NULL,hInstance,NULL);

윈도우 창을 생성합니다.

- haccel = LoadAccelerators(hInstance,MAKEINTRESOURCE(IDR_ACCELERATOR1));

LoadAccelerators함수의 반환 값을 haccel에 저장합니다. LoadAccelerators함수를 살펴봅시다.

이 함수는 지정된 엑셀러레이터를 불러오는 역할을 합니다. 인자들을 살펴봅시다.

* HINSTANCE hInstance

WinMain 함수의 hInstance값을 인자로 받습니다.

* LPCTSTR lpTableName

엑셀러레이터 테이블 명을 받는 인자입니다.

함수가 성공하면 반환값은 액셀러레이터의 핸들입니다.

- while(GetMessage(&msg,0,0,0))
  if (!TranslateAccelerator(hwnd,haccel,&msg)) 
   TranslateMessage(&msg);
   DispatchMessage(&msg);

메세지를 처리하는 부분입니다. 그런데? 여태까지의 코드하고 비교를 해보면 조금 다른 부분이 있습니다. 바로 if문이 들어가 있다는 것입니다. 이 if문은 TranslateAccelerator함수의 결과값을 기준으로 참 거짓을 판별하는데 이 TranslateAccelerator함수를 한 번 살펴봅시다.

메뉴의 액셀러레이터 키를 처리하는 함수라고 나와 있습니다. 인자들을 한 번 살펴봅시다.

* HWND hWnd

메세지를 처리할 핸들입니다.

* HACCEL hAccTable

액셀러레이터의 핸들을 받습니다.

* LPMSG lpMsg

MSG 구조체의 주소를 받습니다.

자 그럼 이제 이 함수가 동작하는 원리를 한번 자세히 살펴봅시다.

일반적으로 이 코드에서 메세지가 오면 GetMessage함수를 사용하여 메세지를 받고 TranslateMessage 함수에서 키보드 관련 메세지를 처리한다음 DispatchMessage함수에 의해 윈도우 프로시저로 전달됩니다. 그런데 말입니다. 우리가 지정한 액셀러레이터를 살펴보면 특정 키를 눌렀을 경우 메뉴 중에서 특정 메뉴의 작동이 일어나는 구조입니다.

특정 메뉴의 작동을 관리하는 메세지는 WM_COMMAND입니다. 그런데? 우리가 지정한 단축키는 실제로 메세지처리에서는 키보드 입력이므로 WM_KEYDOWN 메세지가 발생하게 됩니다. 만약 이 TranslateAccelerator함수를 사용하지 않고 그대로 TranslateMessage함수나 DispatchMessage함수로 가버리면 윈도우 프로시저에서는 WM_COMMAND메세지가 아닌 WM_KEYDOWN메세지의 처리 부분으로 가 버릴 것이고 이는 곧 단축키가 동작이 되지 않는 다는 것을 의미합니다.

그러므로 TranslateAccelerator함수를 사용하여 이 메세지가 키보드 입력 메세지 일 경우 엑셀러레이터 테이블에 지정된 키 값인지 확인을 하여 맞으면 WM_COMMAND메세지로 처리하도록 바꾸는 작업을 하는 것입니다. 이 때 TranslateAccelerator함수의 반환값은 0이 아니며 반대로 키보드 입력 메세지가 아니거나 혹은 액셀러레이터에 등록되지 않은 값이면 이 함수는 0을 반환합니다.

최종적으로 정리하면 이 코드는 단축키가 입력된 WM_KEY 관련 메세지는 WM_COMMAND로 처리를 한 후 if문에 의해 TranslateMessage함수, DispatchMessage함수가 수행되지 않도록 하는 역할을 합니다.

- BOOL WINAPI PageDlgProc(HWND hWnd,UINT Message,WPARAM wParam,LPARAM lParam)

대화 상자에서 발생한 메세지를 처리하는 함수입니다. 여기서 대화상자는 우리가 만든 대화상자를 의미하며 특정 부분을 제외하면 WndProc랑 대단히 유사합니다. 참고로 눈치채신 분들도 계시겠지만 우리가 만든 대화상자는 메모장 - 파일 메뉴의 페이지 설정 대화상자입니다. 즉 이 부분은 페이지 설정 대화상자 작업을 처리하는 부분이라고 생각하시면 됩니다. 물론 전부 구현하진 않았습니다. 시간이 되면 나중에 전부 구현하겠습니다.

- TCHAR *list[] = {L"A3",L"A4",L"B4 (JIS)",L"B5 (JIS)",L"Envelope #10",L"Envelope Monarch",L"Executive",L"Legal",L"Letter",L"Tabloid"};

리스트 배열을 선언하여 위와 같은 문자열로 초기화 시킵니다. 이 문자열은

을 의미합니다.

- int i;

for문에 쓰일 변수 i를 선언하였습니다.

- switch(Message)

메세지를 처리하는 부분입니다.

- case WM_INITDIALOG:

WM_INITDIALOG메세지를 처리하는 부분입니다. 이 메세지는 대화상자를 초기화할 때 발생하는 메세지입니다. 윈도우 프로시저의 WM_CREATE 메세지와 비슷하다고 보시면 됩니다.

- for(i=0;i<10;i++)
    SendDlgItemMessage(hWnd,1137,CB_ADDSTRING,0,(LPARAM)list[i]);

for문을 이용하여 총 10번의 SendDlgItemMessage함수를 호출합니다. 이 SendDlgItemMessage함수는 대화상자 프로시저에서 해당하는 컨트롤에 메세지를 보낼 때 사용합니다. SendMessage함수랑 비슷하다고 보시면 되는데 차이점에 있다면 SendMessage함수는 윈도우 창에 메세지를 보내고 SendDlgItemMessage함수는 윈도우 창의 컨트롤에 메세지를 보내는 점입니다.

인자는 차례대로 핸들,컨트롤의 ID,메세지,WPARAM,LPARAM입니다.

정리하면 대화상자의 1137 컨트롤(용지 크기를 지정하는 콤보박스)에 CB_ADDSTRING메세지를 전송하여 list[i]에 들어있는 문자열을 콤보박스에 추가하라고 볼 수 있습니다.

for문이 진행함에 따라 i값도 달라지니 최종적으론 list 배열에 있는 모든 문자열이 콤보박스에 추가되어집니다.

- SendDlgItemMessage(hWnd,1137,CB_SETCURSEL,1,0);

아까의 그 콤보박스에 CB_SETCURSEL메세지를 전송하여 1번째 문자열을 기본 문자열로 지정하라는 뜻입니다. 콤보박스의 문자열은 차례대로 0~9번을 의미하므로 여기서 1번째 문자열은 A4 문자열을 의미하게 됩니다. 이렇게 하면 나중에 페이지 설정 대화상자를 호출하였을 때 A4가 기본적으로 지정되어 있습니다.

- EnableWindow(GetDlgItem(hWnd,1138),FALSE);

EnableWindow함수를 사용하여 해당하는 핸들의 활성화 여부를 지정합니다. 인자는 차례대로 핸들, 활성화 여부(TRUE,FALSE) 입니다.

그리고 대화 상자 내 컨트롤의 핸들을 가져오기 위해 GetDlgItem함수를 사용합니다. GetDlgItem함수는 지정한 컨트롤의 핸들을 가져오는 함수이며 인자는 차례대로 대화상자의 핸들, 컨트롤의 ID입니다.

정리하면 대화상자의 1138컨트롤을 비활성화 해주는 코드입니다. 여기서 1138컨트롤은 공급 콤보박스를 의미합니다. 이 콤보박스를 비활성화 해주는 이유는 제가 메모장을 켜서 페이지 설정을 대화상자를 호출하니 비활성화 되어 있었기 때문입니다. 물론 오리지널 메모장에선 어떤 조건을 만족하면 다시 활성화 되겠지만 아직 우리는 그 조건이 무엇인지를 모르므로 그냥 넘어가겠습니다.

- SendDlgItemMessage(hWnd,1056,BM_CLICK,0,0);

1056컨트롤(방향의 세로 라디오 버튼)에 BM_CLICK 메세지를 보내 클릭된 상태로 만듭니다.

-  SendDlgItemMessage(hWnd,1155,WM_SETTEXT,0,(LPARAM)L"20");
   SendDlgItemMessage(hWnd,1157,WM_SETTEXT,0,(LPARAM)L"20");
   SendDlgItemMessage(hWnd,1156,WM_SETTEXT,0,(LPARAM)L"25");
   SendDlgItemMessage(hWnd,1158,WM_SETTEXT,0,(LPARAM)L"25");

1155,1157,1156,1158 컨트롤에 WM_SETTEXT 메세지를 보내 지정한 값으로 설정합니다. 이 부분은 페이지 설정 내 여백의 기본값을 설정하는 부분입니다.

- SendDlgItemMessage(hWnd,30,WM_SETTEXT,0,(LPARAM)L"&f");
  SendDlgItemMessage(hWnd,31,WM_SETTEXT,0,(LPARAM)L"&p 페이지");

30 컨트롤(머리글)과 31 컨트롤(바닥글)을 지정한 값으로 설정합니다.

- case WM_CLOSE:
   EndDialog(hWnd,NULL);

WM_CLOSE메세지가 발생하면 대화상자를 종료하는 함수 EndDialog를 호출합니다.

- return FALSE;

WndProc의 DefWindowProc와 비슷한 기능을 합니다.

이제 윈도우 프로시저를 살펴봅시다.

- RECT rt;

영역 지정을 위한 RECT형의 rt변수를 선언합니다.

- switch(Message)

메세지 처리 부분입니다.

- case WM_CREATE:
   GetClientRect(hWnd,&rt);

GetClientRect함수를 이용해 현재 메모장의 크기를 구해 rt 구조체의 각 멤버에 저장시킵니다. 이렇게 해주는 이유는 메모장 윈도우의 크기와 Edit 윈도우의 크기를 똑같이 만들어 주기 위해서 입니다.

- Edit = CreateWindow(L"edit",NULL,WS_CHILD | WS_HSCROLL | WS_VSCROLL | WS_VISIBLE | ES_MULTILINE,rt.left,rt.top,rt.right,rt.bottom,hWnd,
   NULL,Global_hInstance,NULL);

edit 윈도우를 생성합니다. 여기서 크기를 보시면 rt.left,rt.top,rt.right,rt.bottom 이렇게 지정되어 있는 것을 볼 수 있습니다. 즉 메모장 윈도우와 크기가 똑같은 edit 윈도우를 생성한다는 것을 알 수 있습니다.

- case WM_SIZE:
   GetClientRect(hWnd,&rt);

메모장 윈도의 크기가 변경되었을 경우 다시 메모장 윈도우의 크기를 얻어옵니다.

- MoveWindow(Edit,rt.left,rt.top,rt.right,rt.bottom,TRUE);

MoveWindow함수를 이용하여 Edit 윈도우의 크기를 메모장 윈도우의 크기로 바꿉니다. 참고로 이 MoveWindow함수의 마지막 인자가 TRUE일 경우 크기를 바꾼 후 창을 다시 칠하며 FALSE이면 다시 칠하지 않습니다.

- case WM_COMMAND:
   switch(LOWORD(wParam))

메뉴처리 부분인 WM_COMMAND메세지 입니다. 이 WM_COMMAND메세지를 받았을 때 메뉴의 ID는 wParam(4바이트)의 하위 부분(하위 2바이트)에 저장됩니다.

- case N:
     SaveDlgBox(hWnd);
     if(CANCEL != 1)
     {
      DestroyWindow(Edit);
      SendMessage(hWnd,WM_CREATE,0,0);
      SetWindowText(hWnd,L"제목 없음 - 메모장");
     }

   

N 메뉴(새로 만들기)를 눌렀을 경우 SaveDlgBox함수를 호출하여 저장 여부를 묻는 메세지 박스를 호출할 건지 안할 건지의 여부를 체크하고 이 때 박스가 호출되지 않았거나 박스가 호출되었을 때 취소 버튼을 누른 것이 아니라면 eidt 윈도우를 부수고 WM_CREATE 메세지를 전송하여 eidt 윈도우를 다시 만듭니다. 그리고 eidt가 새로 만들어 졌으므로 메모장의 제목표시줄을 기본 상태로 설정합니다.

- else
      CANCEL = 0;
     break;

만약 메세지 박스가 호출되었을 때 취소버튼을 눌렀다면 CANCEL을 0으로 설정하고 아무런 동작도 하지 않습니다.

- case O:
     SaveDlgBox(hWnd);
     if(CANCEL != 1)
      FileOpen(hWnd);
     else
      CANCEL = 0;
     break;

O 메뉴(열기)를 눌렀을 경우 SaveDlgBox함수를 호출하여 저장 여부를 묻는 메세지 박스를 호출할 건지 안할 건지의 여부를 체크하고 이 때 박스가 호출되지 않았거나 박스가 호출되었을 때 취소 버튼을 누른 것이 아니라면 FileOpen함수를 호출합니다.      만약 메세지 박스가 호출되었을 때 취소버튼을 눌렀다면 CANCEL을 0으로 설정하고 아무런 동작도 하지 않습니다.

- case S:
     SaveFile(hWnd);
     break;

S 메뉴(저장)를 눌렀을 경우 SaveFile함수를 호출합니다. 이 부분은 저장을 하는 작업이므로 따로 SaveDlgBox함수를 호출하여 저장을 묻는 메세지박스를 띄우지 않습니다.
- case A:
     SaveFile_as(hWnd);
     break;

A 메뉴(다른 이름으로 저장)를 눌렀을 경우 저장 여부에 상관없이 공용 대화 상자를 띄우기 위해 SaveFile_as함수를 호출합니다.

- case U:
     DialogBox(Global_hInstance,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,PageDlgProc);
     break;

U 메뉴(페이지 설정)를 눌렀을 경우 DialogBox함수를 호출하여 페이지 설정 대화상자를 띄웁니다. DialogBox함수의 인자는 차례대로 hInstance, 대화상자리소스, 대화상자를 소유하는 부모핸들, 대화상자 프로시저 함수 입니다.

- case X:
     SaveDlgBox(hWnd);
     if(CANCEL != 1)
      PostQuitMessage(0);
     else
      CANCEL = 0;
     return 0;

X 메뉴(끝내기)를 눌렀을 경우 SaveDlgBox함수를 호출하여 저장 여부를 묻는 메세지 박스를 호출할 건지 안할 건지의 여부를 체크하고 이 때 박스가 호출되지 않았거나 박스가 호출되었을 때 취소 버튼을 누른 것이 아니라면 PostQuitMessage 함수를 호출합니다. 만약 메세지 박스가 호출되었을 때 취소버튼을 눌렀다면 CANCEL을 0으로 설정하고 아무런 작업도 하지 않습니다.

- switch (HIWORD(wParam)) 

WM_COMMAND메세지에서 wParam의 하위16비트(2바이트)가 메뉴의 ID를 저장한다면 상위16비트는 메뉴의 통지 코드 즉 상태를 저장합니다.

- case EN_CHANGE:
     str_CHANGE = 1;
     break;

EN_CHANGE 메세지는 편집컨트롤의 텍스트가 변경되었을 때 발생하는 메세지입니다. 이럴 경우 문자열 변경 여부를 저장하는 변수 str_CHANGE에 1을 저장합니다.

- case WM_CLOSE:
   SaveDlgBox(hWnd);
   if(CANCEL != 1)
    PostQuitMessage(0);
   else
    CANCEL = 0;
   return 0;

메모장의 닫기 창을 눌렀을 경우 SaveDlgBox함수를 호출하여 저장 여부를 묻는 메세지 박스를 호출할 건지 안할 건지의 여부를 체크하고 이 때 박스가 호출되지 않았거나 박스가 호출되었을 때 취소 버튼을 누른 것이 아니라면 PostQuitMessage 함수를 호출합니다. 만약 메세지 박스가 호출되었을 때 취소버튼을 눌렀다면 CANCEL을 0으로 설정하고 0을 반환하여 DefWindowProc함수에 의해 WM_DESTROY 메세지가 발생하는 것을 막습니다.

이것으로 모든 소스코드를 다 설명했군요. 그런데 아직 해주어야 할 작업이 남아있습니다. 한번 프로그램을 실행 시켜 보십시요.

오디날을 dll에서 찾을 수 없다는 메세지 박스가 나옵니다. 이 오디날이라는 것은 간단단하게 얘기하면 라이브러리에 들어있는 함수들의 번호입니다. 즉 정리하면 링킹작업 때 lib에서 가져온 함수 중 345번의 함수를 DLL파일에서 찾지 못해 파일을 실행할 수 없다 라고 볼 수 있겠네요. 아무래도 이 345번의 함수는 TaskDialogIndirect인 것 같습니다. 그러면 왜 이 함수를 찾지 못하는 것일까요?

기본적으로 lib파일은 DLL을 로드하거나 DLL에 있는 함수를 호출하기 위한 코드만 들어 있습니다. 예를 든다면 우리가 포함시킨 comctl32.lib 파일에는 COMCTL32.dll을 로드하는 코드와 comctl32.dll에 있는 TaskDialogIndirect함수를 호출하는 코드가 들어있는 것이지요. 그리고 정상적으로는 COMCTL32.dll에 TaskDialogIndirect 함수가 들어있습니다. 그런데 왜 찾을 수 없다고 뜨는 것일까요? 그것은 바로 이 프로그램이 낮은 버전의 COMCTL32.dll파일을 참고하고 있기 때문입니다.

다른건 자세히 모르겠으나 이 visual studio 2010로 빌드한 실행파일에서 COMCTL32.dll을 참조하면 기본적으로 낮은 버전의 COMCTL32.dll을 들고옵니다. 그리고 이 낮은 버전의 COMCTL32.dll에는 TaskDialogIndirect함수가 들어있지 않습니다. 그래서 위와 같은 문제가 발생하는 것입니다.

그러므로 우리는 다른 버전의 dll을 들고오도록 프로그램을 수정할 필요가 있습니다. 이때 수정하는 부분이 바로 manifest입니다.

위키피디아에서 manifest의 정의를 살펴보면 프로그램의 이름, 버전 번호 및 구성 파파일을 설명하는 파일이라고 나와 있네요. 자세히는 모르겠지만 일단 실행파일이 로드하는 DLL파일의 버전을 바꿀려면 이 부분을 건드려야 합니다.

ResEdit프로그램을 열어 우리가 만든 메모장.exe를 불러와 봅시다.(리소스가 아닌 실팽파일을 불러와야 합니다.)

우리가 편집한 리소스하고는 다르게 manifest부분이 자동으로 생성된 것을 알 수 있습니다. 기존의 manifest 파일을 지운다음 manifest 리소스를 새로 추가합시다.

그러면 위와 같은 메세지 박스가 뜨는데 ResEdit에서 자동으로 manifest파일을 만들까요 하고 묻는 메세지 박스입니다. 예를 누릅시다.

그러면 프로그램에 맞는 manifest가 자동으로 생성되어집니다. 이 상태로 저장을 하고 파일을 한 번 실행시켜 봅시다.

우리가 만든 메모장 파일이 정상적으로 실행되는 것을 알 수 있습니다. 한번 제대로 동작이 되는지 살펴보십시요.

파일을 저장할 때 입니다. 왼쪽이 우리가 만든 메모장, 우측이 오리지널 메모장입니다.오리지널 메모장은 공용 대화 상자에 인코딩 상자가 따로 추가되어 있는 것을 알 수 있습니다.

페이지 설정 대화상자 부분입니다. 역시 왼쪽이 우리가 만든 대화상자, 우측이 오리지널 대화상자입니다. 완전 똑같지는 않지만 살짝 비슷하지 않습니까?

네 이것으로 메모장 만들기를 마치겠습니다. 시간이 된다면 소스코드를 더 추가해보도록 하겠습니다.

Posted by englishmath
,