안녕하십니까? 이번 포스팅에선 제가 만든 넷버스 알고리즘을 한 번 포스팅해보도록 하겠습니다.

넷버스란?

아주 오래전에 만들어진 악성 프로그램 중 하나로 사용자의 PC를 감염시켜 원격조종을 하는 것이 주 기능입니다.

일단 넷버스 알고리즘을 살펴보기 전에 다음과 같은 주의사항을 숙지하시기 바랍니다.

1. 앞으로 소개할 프로그램은 악성기능이 내포되어 있으므로 마음대로 악용을 하지 않습니다.

2. 위 사항을 어겨 불이익을 받을 시 이 블로그는 책임을 지지 않습니다.

3. 악성행위의 목적이 아닌 공부 목적으로 제작한 것이므로 독자분들은 위의 주의사항을 숙지하시고 협조 부탁드립니다.

자 이제 한 번 제가 만든 넷버스 알고리즘을 하나씩 살펴봅시다.

1. 넷버스는 TCP 소켓을 이용하여 통신을 한다.

2. 넷버스는 두개의 프로그램으로 이루어져 있으며 클라이언트(공격자) - 서버(감염자) 통신 방식을 이용한다.

3. 감염자가 자신의 PC에 악성코드를 실행시키면 공격자가 해당 감염자의 IP주소를 입력하여 소켓을 연결한다.

4. 소켓이 연결되면 send, recv api함수를 사용하여 데이터를 빼내온다.

5. 감염자의 PC는 공인IP를 사용하여야 한다. (사설 IP인 경우 공격자와 감염자가 같은 네트워크 상에 속해 있어야 하며 그게 아니라면 감염자에게 사설IP를 제공하는 기기(공유기 등)에 포트포워딩을 해주어야 한다.

6. 필자가 만든 넷버스는 오리지널 넷버스의 겉모습만 비슷하게 만들었을 뿐 제공해주는 기능에는 차이가 있다.

7. 일부 백신은 해당 프로그램을 악성코드로 취급할 수 있다.

이정도 등이 있습니다. 자 그럼 다음 포스팅에선 리소스를 살펴보겠습니다.




Posted by englishmath
,

안녕하십니까? 이번 포스팅에서는 제가 만든 작업관리자의 코드를 보여드리겠습니다.먼저 코드를 봅시다.

예전에 설명한 코드들은 생략하고 살펴보겠습니다.

- #include <uxtheme.h>

SetWindowTheme함수를 사용하기 위해 선언한 헤더파일입니다.

- #include <CommCtrl.h>

컨트롤들의 디자인을 바꾸기 위해 선언한 헤더파일입니다. 이 헤더파일을 사용할 경우 manifest를 변경해주어야 합니다. 변경하는 법은 메모장 만들기 포스팅에서 언급하였으므로 생략하겠습니다.

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

   #pragma comment(lib, "uxtheme.lib")

각각의 라이브러리 파일을 링커에 추가시켜 줍니다.

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

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

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

  BOOL WINAPI EnumWindowsProc(HWND hwnd,LPARAM lParam);

  BOOL WINAPI EnumDesktopProc(LPTSTR lpszDesktop,LPARAM lParam);

각각의 프로시저함수들을 선언하는 부분입니다. 이것은 나중에 자세히 설명드리겠습니다.

- void InsertItemDlglist(HWND *DesktopHWND1, TCHAR **DesktopWND_title1, int count);

  int checkDesktopWND_title(TCHAR *DesktopWND_title1);

  int CMP_DesktopWND_title(void);

  void free_DesktopWND_title(void);

  void free_DesktopWND_title2(void);

  void LVHeaderSort(HWND hWnd,TCHAR *DlgListHeader_Str[],int columnindex);

  void asce_sort(void);

  void desc_sort(void);

  void GetLVHeaderSort(void);

  void MoveWND(HWND hWnd);

앞 포스팅에서 설명드린 함수들을 선언합니다.

- HINSTANCE Global_hInstance;

  HWND Global_MainHWND;

  HWND Global_TabDlgHWND;

WinMain의 hInstance와 각각의 대화상자 핸들을 전역변수에 저장시키기 위해 선언한 전역변수입니다.

- HIMAGELIST Global_ImageList;

이미지리스트를 생성후 전역변수에 저장시키기 위해 선언하였습니다.

- HWND Global_DesktopHWND[100];

  TCHAR *Global_DesktopWND_title[100];

  TCHAR *Global_DesktopWND_title2[100];

데스크탑에서 가져온 윈도우의 핸들과 타이틀을 저장할 배열을 선언합니다.

- int Global_hwnd_count = 0;

  int Global_Dlglist_count = 0;

데스크탑에서 가져온 윈도우의 개수와 리스트 뷰에 추가된 윈도우의 개수를 받을 전역변수를 선언하고 0으로 초기화합니다.

- POINT Global_MINSIZEcoordinate;

작업관리자의 최소 크기를 지정해주기 위해 POINT 구조체 변수를 전역변수로 선언하였습니다. POINT구조체의 멤버는 x와 y이며 x와 y좌표를 저장받습니다.

이제 WinMain 함수를 살펴봅시다.

- Global_hInstance = hInstance;

hInstance값을 전역변수에 저장시킵니다.

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

리소스에서 액셀러레이터를 불러와 변수에 저장시킵니다.

- hWnd = CreateDialog(hInstance,MAKEINTRESOURCE(IDD_DIALOG1),NULL,MainDlgProc);

CreateDialog함수를 사용하여 대화상자를 생성합니다. 이 때 생성하는 대화상자는 IDD_DIALOG1이므로 Tab컨트롤과 static컨트롤이 포함되어 있는 메인대화상자입니다. 이 대화상자는 함수의 마지막 인자인 MainDlgProc함수에서 동작하며 이 때 핸들을 hWnd에 저장시킵니다.

- while(GetMessage(&msg,0,0,0))

if (!TranslateAccelerator(hWnd,haccel,&msg))

EnumDesktops(GetProcessWindowStation(),EnumDesktopProc,0);

TranslateMessage(&msg);

DispatchMessage(&msg);

return 0;

메세지 루프 부분입니다. 다만 여태까지와의 코드에서 다른 점이 있다면 액셀러레이터메세지를 대화상자(hWnd)에서 처리한다는 부분과 EnumDesktops함수가 포함되어있다는 것을 알 수 있습니다. EnumDesktops함수를 살펴봅시다.

지정된 윈도우 스테이션에 포함된 데스크탑을 전부 열거해주는 함수라고 적혀있습니다. 각 인자들을 살펴봅시다.

* HWINSTA hwinsta

윈도우 스테이션의 핸들을 인자로 받습니다.

* DESKTOPENUMPROC lpEnumFunc

윈도우 스테이션에서 가져온 데스크탑을 처리할 콜백 함수의 포인터입니다.

* LPARAM lParam

콜백함수에 추가로 전달될 값을 인자로 받습니다.

즉 정리하면 이 함수는 윈도우 스테이션에서 데스크탑을 전부 가져와 사용자가 처리할 수 있도록 해주는 함수입니다. 일단 이를 자세히 이해할려면 윈도우 스테이션을 알 필요가 있습니다.

윈도우스테이션은 쉽게 얘기하면 윈도우즈에 포함된 하나의 영역이라고 보시면 됩니다. 윈도우즈 운영체제는 여러개의 세션으로 나뉘어지고 세션은 여러개의 윈도우스테이션을 포함하고 있으며 윈도우 스테이션은 여러개의 데스크탑을 포함하고 있는 구조이지요. 

그리고 우리는 작업관리자를 구현하기 위해 데스크탑에서 핸들을 가져와야 하므로 데스크탑을 가져오기 위해 윈도우 스테이션을 가져와야 합니다. 이 때 윈도우 스테이션을 가져오는 함수가 바로 GetProcessWindowStation함수입니다.

GetProcessWindowStation함수는 현재 프로세스와 연관된 윈도우 스테이션을 가져오는 함수이며 따로 사용자가 임의로 프로세스의 윈도우 스테이션을 바꾼 것이 아니라면 기본적으로 WinSta0 윈도우 스테이션을 들고오게 됩니다.

그리고 그렇게 가져온 윈도우 스테이션을 인자값으로 준 EnumDesktopProc함수에서 처리하게 됩니다.

이제 WndProc함수를 설명드릴 차례인데 우리는 윈도우가 아닌 대화상자에서 작업을 하므로 이 WndProc함수는 그냥 기본틀만 잡았습니다. 그러므로 생략하겠습니다.

자 그러면 MainDlgProc함수를 살펴봅시다. 이 함수는 메인 대화상자의 메세지를 처리하는 함수입니다.

- TCITEM Tab;

TCITEM 구조체 변수 Tab을 선언합니다. 이 구조체를 살펴봅시다.

이 구조체는 탭 컨트롤과 관련된 속성을 지정하기 위한 멤버가 들어있습니다. 탭 컨트롤의 정보를 받거나 혹은 추가할 때 사용하는 구조체입니다. 사용한 멤버들만 살펴봅시다.

* UINT mask

어떠한 멤버를 유효멤버로 정할지를 값으로 받는 멤버입니다. 이 값에 따라 유효멤버가 바뀌며 특정 멤버는 유효멤버가 아니면 어떤 값이 들어가든 무효가 되버립니다.

* LPTSTR pszText

탭 컨트롤에 지정할 문자열을 값으로 받습니다.

- TCHAR *Tab_String[] = {L"응용 프로그램",L"프로세스",L"서비스",L"성능",L"네트워킹",L"사용자"};

각각의 탭 컨트롤에 들어갈 문자열을 선언합니다.

- int i;

포커싱된 탭 컨트롤의 인덱스 값을 받기 위해 선언하였습니다.

- LPMINMAXINFO MINMAX;

작업관리자의 최소크기를 변경하기 위해 LPMINMAXINFO 구조체를 선언합니다. 이 구조체를 한 번 살펴봅시다.

윈도우의 최대 크기 및 최소크기를 저장한다고 되어 있습니다. 사용한 멤버만 살펴봅시다.

*POINT ptMinTrackSize

윈도우의 최소크기를 값으로 받는 멤버변수입니다. POINT 구조체의 멤버변수인 x가 최소넓이가 되며 y는 최소 높이가 됩니다.

- WINDOWINFO window;

윈도우의 정보를 담는 WINDOWINFO구조체 변수를 선언합니다.

- switch(Message)

case WM_INITDIALOG:

대화상자가 생성될 때 발생하는 메세지를 받았을 때 아래 코드를 수행합니다.

- Global_MainHWND = hWnd;

핸들을 전역변수에 저장시킵니다.

- SetWindowPos(hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE);

SetWindowPos함수를 사용하였습니다. 이 함수를 살펴봅시다.

윈도우의 크기와 위치, z축 순서를 변경시켜주는 함수입니다. z축 순서를 변경시킨다는 점에서 MoveWindow함수와 차이가 나는군요. 인자들을 살펴봅시다.

* HWND hWnd

변경될 윈도우의 핸들입니다.

* HWND hWndInsertAfter

윈도우의 Z축 위치를 설정합니다. 여기서는 HWND_TOPMOST값을 주어 작업관리지가 어떠한 윈도우보다도 높은 곳에 위치하도록 합니다.

* int   X

윈도우의 x좌표입니다.

* int   Y

윈도우의 y좌표입니다.

* int   cx

윈도우의 너비입니다.

* int   cy

윈도우의 높이입니다.

* UINT uFlags

윈도우에 적용할 추가옵션입니다. 여기서는 SWP_NOMOVE와 SWP_NOSIZE 값을 주어 이 함수가 실행될 때 작업관리자의 위치와 크기가 변경되지 않도록 하였습니다.

- window.cbSize = sizeof(window);

window의 cbSize멤버값에 구조체의 크기를 넣어줍니다.

- GetWindowInfo(hWnd,&window);

GetWindowInfo함수를 사용하여 현재 메인 대화상자의 정보를 window구조체에 저장시킵니다.

- Global_MINSIZEcoordinate.x = window.rcWindow.right - window.rcWindow.left;

POINT 구조체 전역변수의 멤버인 x에 메인 대화상자의 right좌표값에서 left좌표값을 뺀 값을 저장시킵니다. 이렇게 하면 메인 대화상자가 생성될 때의 너비값이 x멤버에 저장됩니다. 즉 이 값이 메인 대화상자의 최소 너비값이 됩니다.

- Global_MINSIZEcoordinate.y = window.rcWindow.bottom - window.rcWindow.top;

POINT 구조체 전역변수의 멤버인 y에 메인 대화상자의 bottom좌표값에서 top좌표값을 뺀 값을 저장시킵니다. 이렇게 하면 메인 대화상자가 생성될 때의 높이값이 ㅛ멤버에 저장됩니다. 즉 이 값이 메인 대화상자의 최소 높이값이 됩니다.

- SendMessage(hWnd,WM_SETICON,ICON_BIG,(LPARAM)LoadIcon(Global_hInstance,MAKEINTRESOURCE(IDI_ICON1)));

메인 대화상자에 WM_SETICON메세지를 보내 아이콘을 작업관리자 아이콘으로 변경합니다.

- SetMenu(hWnd,LoadMenu(Global_hInstance,MAKEINTRESOURCE(FileMenu)));

리소스에 작성된 메뉴를 메인 대화상자에 추가시킵니다. 

- SetWindowText(hWnd,L"Windows 작업 관리자");

메인 대화상자의 타이틀을 Windows 작업 관리자로 변경합니다.

- for(i=0;i<6;i++)

Tab.mask = TCIF_TEXT;

Tab.pszText = Tab_String[i];

      SendDlgItemMessage(hWnd,Tab1,TCM_INSERTITEM,i,(LPARAM)&Tab);

for문과 SendDlgItemMessage함수를 사용하여 총 6개의 탭을 탭컨트롤에 추가시킵니다. 문자열은 앞에서 선언한 Tab_String값입니다.

- SendDlgItemMessage(hWnd,Tab1,TCM_SETCURSEL,0,0);

탭컨트롤에 TCM_SETCURSEL메세지를 보내 wParam(0) 탭을 선택하도록 합니다. 즉 이말은 기본 포커싱을 0번 째 탭(응용프로그램)으로 지정하겠다는 뜻입니다.

-    CreateDialog(Global_hInstance,MAKEINTRESOURCE(Tab1_DIALOG),hWnd,Tab1DlgProc);

메인 대화상자의 설정이 모두 끝났으면 리스트뷰가 들어있는 Tab1_DIALOG대화상자를 생성합니다. 프로시저함수는 Tab1DlgProc입니다.

- case WM_PAINT:

무효화 영역이 생겨서 다시 그려야 할 경우 아래의 코드를 수행합니다.

- i = SendDlgItemMessage(hWnd,Tab1,TCM_GETCURFOCUS,0,0);

현재 포커스된 탭의 인덱스를 가져와 i에 저장시킵니다.

- switch(i)

case 0:

  ShowWindow(Global_TabDlgHWND,SW_HIDE);

ShowWindow(Global_TabDlgHWND,SW_SHOW);

break;

default:

break;

탭의 인덱스가 0(응용프로그램)이면 메인 대화상자를 숨기고 다시 보여줍니다.

만약 다른 인덱스라면 대화상자를 보여주지 않습니다.

- case WM_CLOSE:

PostQuitMessage(0);

메인 대화상자의 닫기버튼을 눌렀을 경우 PostQuitMessage함수를 호출하여 프로그램을 종료시킵니다.

- case WM_SIZE:

대화상자의 크기 관련 메세지를 받을 경우 아래의 코드를 수행합니다.

- if(IsIconic(hWnd))

SetWindowPos(hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE);

IsIconic함수의 결과값이 참일 경우 SetWindowPos함수를 사용하여 메인 대화상자의 Z위치를 HWND_TOPMOST로 줍니다. IsIconic함수는 지정한 핸들이 최소화 된 핸들일 경우 참을 반환하는 함수이며 보통 최소화된 창을 눌러 크기를 복구 시킬 때에는 Z축 위치가 변경되므로 Z축의 위치를 원상태로 복구하기 위해 작성한 코드입니다.

- MoveWND(hWnd);

그리고 위의 if문과는 관계 없이 MoveWND함수를 호출하여 메인 대화상자의 크기에 맞게 컨트롤들의 크기와 위치를 변경시킵니다.

- case WM_GETMINMAXINFO:

메인 대화상자의 크기나 위치가 변경되었을 경우 발생하는 메세지입니다. 이 때 lParam은 MINMAXINFO 구조체이며 이 구조체의 값을 변경함으로써 메인 대화상자의 최대크기나 최소크기를 변경할 수 있습니다.

- MINMAX = (LPMINMAXINFO)lParam;

MINMAXINFO구조체 값인 lParam을 MINMAX에 저장시킵니다.

- MINMAX->ptMinTrackSize = Global_MINSIZEcoordinate;

MINMAX구조체의 ptMinTrackSize멤버에 아까 메인대화상자가 생성될 때 지정해준 최소크기 값이 들어있는 Global_MINSIZEcoordinate구조체 값을 넣어줍니다. 이렇게 하면 사용자가 크기 변경을 요청해도 Global_MINSIZEcoordinate의 크기보다 작게는 변경되지 않습니다.

- case WM_NOTIFY:

메인 대화상자와 관련된 이벤트가 발생했을 경우 아래의 코드를 수행합니다. 이 때 lParam은 LPNMHDR구조체 값입니다. 이 구조체를 한 번 살펴봅시다.

이벤트에 대한 정보를 담는 구조체라고 되어 있습니다. 이중 사용한 멤버만 살펴봅시다.

* UINT code

어떠한 이벤트가 발생했는지를 알려주는 코드입니다.

- switch(((LPNMHDR)lParam)->code)

NMHDR 멤버의 code값을 switch함수의 인자로 주어 각 이벤트마다 다른 처리를 하도록 하였습니다.

- case TCN_SELCHANGE:

code값이 TCN_SELCHANGE일때의 처리입니다. 즉 탭 컨트롤이 변경되었다는 것을 알알리는 코드이지요. 탭 컨트롤이 변경되었을 경우 아래의 코드를 수행합니다.

- i = SendDlgItemMessage(hWnd,Tab1,TCM_GETCURFOCUS,0,0);

현재 선택된 탭 컨트롤의 인덱스를 가져옵니다.

- switch(i)

case 0:

ShowWindow(Global_TabDlgHWND,SW_SHOW);

break;

default:

ShowWindow(Global_TabDlgHWND,SW_HIDE);

break;

0번째 탭(응용 플로그램)을 클릭했을 경우 메인 대화상자를 보여주고 그 외의 탭이라면 대화상자를 감춥니다.

- return FALSE;

WndProc함수의 DefWindowProc함수와 동일한 기능을 합니다.

이제 리스트 뷰가 포함되어 있는 대화상자 프로시저를 살펴봅시다.

- LVCOLUMN DlgListHeader = {0};

LVCOLUMN구조체 변수를 선언하고 0으로 초기화합니다. LVCOLUMN구조체를 살펴봅시다. 

리스트뷰의 열에 관한 정보를 담고 있는 구조체라고 되어 있습니다. 이 구조체는 리스트뷰 열을 추가할 때 사용합니다. 사용한 멤버만 살펴봅시다.

* UINT mask

어떠한 멤버를 유효멤버로 정할지를 값으로 받는 멤버입니다. 이 값에 따라 유효멤버가 바뀌며 특정 멤버는 유효멤버가 아니면 어떤 값이 들어가든 무효가 되버립니다.

* int cx

열의 너비값을 받는 멤버입니다.

* LPTSTR pszText

열의 문자열을 받는 멤버입니다.

- TCHAR *DlgListHeader_Str[] = {L"작업",L"상태"};

열의 문자열들을 저장할 변수를 선언한 후 문자열을 집어넣습니다.

- int i;

for문에 사용할 변수를 선언하였습니다.

- int size = 270;

열의 너비값을 저장하는 변수 size를 선언하였습니다.

- HWND LVHeader;

  HDITEM HeaderItem;

리스트뷰 헤더의 핸들을 저장할 변수와 헤더의 정보를 저장할 HDITEM 구조체 변수를 선언합니다.

- int itemindex;

리스트뷰의 항목들의 인덱스를 저장할 변수를 선언하였습니다.

- switch(Message)

case WM_INITDIALOG:

리스트뷰를 담고 있는 대화상자가 생성될 때 아래코드를 수행합니다.

- Global_ImageList = ImageList_Create(16,16,ILC_MASK | ILC_COLOR32,10,0);

ImageList_Create함수를 사용하여 이미지리스트를 생성후 그 핸들을 Global_ImageList에 저장합니다. ImageList_Create함수를 살펴봅시다.

함수명 그대로 이미지리스트를 생성하는 함수입니다. 인자들을 살펴봅시다.

* int cx

이미지리스트에 저장될 이미지의 너비입니다. 우리는 작은 아이콘을 넣어야 하므로 이미지의 너비를 16으로 주었습니다.

* int cy

이미지리스트에 저장될 이미지의 높이입니다. 우리는 작은 아이콘을 넣어야 하므로 이미지의 높이를 16으로 주었습니다.

* UINT flags

이미지 리스트의 추가옵션을 받는 인자입니다. 어기서는 ILC_MASK값과 ILC_COLOR32값을 주어 32bit DIB섹션을 사용하도록 하였습니다.

* int cInitial

이미지리스트에 저장될 이미지의 개수입니다. 여기서는 10개로 주었습니다.

* int cGrow

이 인자는 무슨 값을 받는지를 몰라 그냥 NULL값을 주었습니다.

즉 정리하면 우리는 16*16 크기의 이미지를 32bit DIB섹션을 사용하여 10개 저장하는 이미지리스트를 생성하여 그 핸들을 전역변수에 저장시켰습니다.

- Global_TabDlgHWND = hWnd;

리스트뷰가 들어있는 대화상자의 핸들을 전역변수에 저장힙니다.

- for(i=0;i<2;i++)

리스트뷰의 헤더를 추가하기 위해 for문을 돌립니다.

- DlgListHeader.mask = LVCF_TEXT | LVCF_WIDTH;

mask멤버에 LVCF_TEXT | LVCF_WIDTH 값을 넣습니다. LVCF_TEXT는 pszText멤버를, LVCF_WIDTH는 cx멤버를 유효화시킵니다.

- DlgListHeader.cx = size;

cx멤버에 헤더의 너비값이 들어있는 size값을 넣습니다.

- DlgListHeader.pszText = DlgListHeader_Str[i];

pszText에 DlgListHeader_Str배열의 i번째 문자열을 대입합니다. 이렇게 하면 차례대로 작업, 상태가 들어가게 됩니다.

- SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_INSERTCOLUMNW,i,(LPARAM)&DlgListHeader);

리스트뷰에 LVM_INSERTCOLUMNW메세지를 보내 리스트뷰의 헤더를 추가합니다. 이때 헤더의 인덱스는 wParam(i)입니다.

- size = 60;

첫번째 헤더(작업)추가가 끝났으면 다음 헤더(상태)의 너비를 size값에 넣습니다.

for문이 끝나면 아래의 코드를 수행합니다.

- LVHeader = (HWND)SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_GETHEADER,0,0); 

LVM_GETHEADER메세지를 보내 방금 추가한 헤더의 핸들을 얻어옵니다.

- HeaderItem.mask = HDI_TEXT | HDI_FORMAT;

  HeaderItem.fmt = HDF_SORTUP | HDF_STRING;

  HeaderItem.pszText = DlgListHeader_Str[0];

  SendMessage(LVHeader,HDM_SETITEM,0,(LPARAM)&HeaderItem);

HDITEM구조체의 멤버값에 값을 넣고 HDM_SETITEM메세지를 보내 0번째 헤더를 수정합니다. 이 코드를 수행하면 작업관리자를 처음으로 실행할 때 기본적으로 작업 헤더가 오름차순을 뜻하는 삼각형을 갖게 됩니다.

-    SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_SETEXTENDEDLISTVIEWSTYLE,0,LVS_EX_FULLROWSELECT);

리스트뷰에 LVM_SETEXTENDEDLISTVIEWSTYLE메세지를 보내 리스트뷰의 확장스타일을 설정합니다. 이때 스타일은 lParam값인 LVS_EX_FULLROWSELECT스타일이며 이 값을 주면 리스트뷰에서 선택한 아이템과 아이템의 하위 항목이 동시에 표시됩니다.만약 이 스타일을 주지 않는다면 아이템을 선택했을 때 아이템의 하위항목은 표시되지 않습니다.

- SetWindowTheme(GetDlgItem(hWnd,Tab1_DLG_List),L"Explorer",NULL);

이 함수는 지정한 핸들의 시각적 스타일을 지정한 응용프로그램의 시각적 스타일로 바꿔주는 함수입니다. 즉 우리가 만든 리스트뷰의 시각적 스타일을 응용프로그램인 Explorer(탐색기)의 시각적 스타일로 바꿔준다고 보시면 됩니다. 세번째 인자는 무슨 뜻인지 몰라 NULL값을 주었습니다.

- SetFocus(GetDlgItem(hWnd,Tab1_DLG_List));

키보드 포커스를 리스트뷰에 맞춥니다. 이 코드가 수행되면 작업관리자가 실행될 때 리스트뷰에서 키보드 값을 받게 됩니다. 만약 이코드가 없다면 자동으로 키보드 포커싱이 탭 컨트롤로 지정됩니다.

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

  EnableWindow(GetDlgItem(hWnd,change),FALSE);

처음 작업관리자가 실행될 때에는 아무런 아이템도 선택되지 않으므로 작업 끝내기 버튼과 전환 버튼을 비활성화시킵니다.

- ShowWindow(hWnd,SW_SHOW);

모든 설정이 다 끝났으면 SW_SHOW옵션을 주어 윈도우를 보이게 만듭니다.

- case WM_COMMAND:

사용자가 메뉴나 컨트롤을 눌렀을 경우 발생하는 메세지이며 아래의 코드를 수행합니다.

- switch(LOWORD(wParam))

메뉴나 컨트롤의 ID값이 들어있는 하위 wParam값에 따라 처리가 달라집니다.

- case TaskExit:

itemindex = SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_GETNEXTITEM,-1,LVNI_SELECTED);

SendMessage(Global_DesktopHWND[itemindex],WM_CLOSE,0,0);

작업 끝내기 버튼을 눌렀을 경우 리스트뷰에 LVM_GETNEXTITEM메세지를 보내 리스트뷰의 항목을 전부 검사합니다. 이 때 LVNI_SELECTED옵션이 적용된 항목을 찾아 그 항목의 인덱스를 itemindex에 저장합니다. 여기서 LVNI_SELECTED옵션은 선택된 항목을 뜻합니다.

항목의 인덱스를 받았다면 그 인덱스에 해당하는 핸들에 WM_CLOSE메세지를 보내 닫기를 요청합니다.

- case change:

ShowWindow(Global_MainHWND,SW_MINIMIZE);

  itemindex = SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_GETNEXTITEM,-1,LVNI_SELECTED);

SwitchToThisWindow(Global_DesktopHWND[itemindex],TRUE);

전환 버튼을 눌렀을 경우에는 먼저 작업관리자에 SW_MINIMIZE옵션을 적용하여 최소화 시킨후 해당하는 리스트뷰 항목의 인덱스를 구해 SwitchToThisWindow함수를 사용하여 포커스를 인덱스에 해당하는 핸들로 맞춘 후 포그라운드로 가져옵니다.

- case WM_NOTIFY:

switch(((LPNMHDR)lParam)->code)

이벤트 통지에 따라 아래의 코드를 수행합니다.

- case LVN_COLUMNCLICK:

switch(((LPNMLISTVIEW)lParam)->iSubItem)

리스트뷰의 칼럼을 클릭했다는 메세지를 받으면 lParam은 NMLISTVIEW구조체의 주소값을 받게 됩니다. 이 때 NMLISTVIEW구조체의 멤버중 하나인 iSubItem을 확인하여 어떤 칼럼을 클릭했는지에 따라 처리를 달리합니다.

- case 0:

LVHeaderSort(hWnd,DlgListHeader_Str,0);

break;

  case 1:

   LVHeaderSort(hWnd,DlgListHeader_Str,1);

break;

각각 해당하는 칼럼을 눌렀을 경우 LVHeaderSort함수를 호출하여 상황에 따라 누른 칼럼을 오름차순 혹은 내림차순으로 바꾸거나 아니면 아무것도 나타내지 않는 빈 헤더로 바꿉니다. 이때 함수의 세번째 인자값은 누른 칼럼의 인덱스 값입니다. 

- case LVN_ITEMCHANGED:

리스트뷰의 항목변경이 일어난 경우 발생하는 메세지입니다. 이때 아래의 코드를 수행합니다.

- switch(((LPNMLISTVIEW)lParam)->uNewState)

lParam의 멤버인 uNewState값에 따라 처리를 달리합니다. uNewState는 리스트뷰의 항목에 대한 상태를 나타냅니다. 

- case 0:

EnableWindow(GetDlgItem(hWnd,TaskExit),FALSE);

EnableWindow(GetDlgItem(hWnd,change),FALSE);

uNewState값이 0이라는 것은 어떠한 항목도 선택되지 않았음을 나타냅니다. 이럴 때에는 작업 끝내기 버튼과 전환 버튼을 비활성화 시킵니다.

- case 3:

EnableWindow(GetDlgItem(hWnd,TaskExit),TRUE);

EnableWindow(GetDlgItem(hWnd,change),TRUE);

uNewState값이 3이라는 것은 어떠한 항목이 선택되었음을 나타냅니다. 이럴 때에는 작업 끝내기 버튼과 전환 버튼을 활성화 시킵니다.

자 다음으로 EnumDesktopProc함수를 살펴봅시다. 이 함수는 앞의 WinMain함수에서 등장한 EnumDesktops함수의 프로시저함수입니다.

- HDESK desktop;

데스크탑의 핸들을 받는 변수를 선언합니다.

- desktop = OpenDesktop(lpszDesktop,0,NULL,DESKTOP_READOBJECTS);

OpenDesktop함수를 사용하여 지정한 데스크탑을 엽니다. OpenDesktop함수를 살펴봅시다.

말 그대로 지정한 데스크탑을 여는 함수입니다. 인자들을 살펴봅시다.

* LPTSTR lpszDesktop

데스크탑 명을 인자로 받습니다. 여기서는 EnumDesktopProc함수가 반환하는 데스크탑의 명인 lpszDesktop을 인자로 주었습니다.

* DWORD dwFlags

0과 DF_ALLOWOTHERACCOUNTHOOK 둘중 하나의 값이 될 수 있다고 합니다. 저는 이 인자에 관해 잘 모르므로 그냥 0으로 주었습니다.

* BOOL fInherit

상속 관련 인자입니다. 역시 잘 모르므로 NULL값을 주었습니다.

* ACCESS_MASK dwDesiredAccess

권한을 설정하는 부분입니다. 우리는 데스크탑에서 동작하는 윈도우를 읽어야 하므로 그에 맞는 권한인 DESKTOP_READOBJECTS권한을 주었습니다. 

정리하면 EnumDesktopProc함수에서 받는 데스크탑을 여는 함수라고 보시면 됩니다. EnumDesktopProc는 여러개의 데스크탑을 반환하지만 개발자가 따로 코드를 작성하지 않는 이상 기본적으로 Default 데스크탑을 반환합니다. Default데스크탑은 사용자의 데스크탑이라고 보시면 됩니다.

- Global_hwnd_count = 0;

핸들의 개수를 0으로 초기화합니다.

- EnumDesktopWindows(desktop,EnumWindowsProc,NULL);

EnumDesktopWindows함수를 호출합니다. 이 EnumDesktopWindows함수는 인자로 받은 데스크탑과 관련된 모든 윈도우를 열거하는 함수입니다. 이 때 프로시저함수는 EnumWindowsProc함수입니다.

- GetLVHeaderSort();

리스트뷰 헤더의 정렬상태를 읽어와 그에 맞게 배열을 정렬시키는 함수GetLVHeaderSort를 호출합니다.

- if(Global_Dlglist_count == 0)

InsertItemDlglist(Global_DesktopHWND,Global_DesktopWND_title,Global_hwnd_count);

리스트뷰에 추가된 항목의 개수가 0인 경우 InsertItemDlglist함수를 호출하여 리스트뷰에 항목을 추가합니다.

- else if(Global_Dlglist_count != Global_hwnd_count)

free_DesktopWND_title2();

InsertItemDlglist(Global_DesktopHWND,Global_DesktopWND_title,Global_hwnd_count);

리스트뷰에 추가된 항목의 개수와 데스크탑에서 가져온 핸들의 개수가 다른 경우 DesktopWND_title2배열을 해제하고 InsertItemDlglist함수를 호출합니다. 

추가로 if부분의 코드는 처음 리스트뷰에 항목을 추가하는 부분이기 때문에 DesktopWND_title2를 해제해주지 않습니다. 동적할당이 되어 있지 않으니까요.

- else if(Global_Dlglist_count == Global_hwnd_count)

if(CMP_DesktopWND_title())

InsertItemDlglist(Global_DesktopHWND,Global_DesktopWND_title,Global_hwnd_count);

리스트뷰에 추가된 항목의 개수와 데스크탑에서 가져온 핸들의 개수가 같은 경우 CMP_DesktopWND_title함수를 호출하여 리스트뷰에 추가된 윈도우들의 타이틀과 데스크탑에서 가져온 윈도우들의 타이틀을 비교합니다. 만약 하나의 윈도우라도 타이틀이 서로 다르다면 InsertItemDlglist함수를 호출하여 리스트뷰를 다시 작성합니다.

- CloseDesktop(desktop);

  free_DesktopWND_title();

모든 처리가 다 끝났으면 열린 데스크탑을 닫고 DesktopWND_title 배열을 해제시켜줍니다.

드디어 마지막이군요. EnumDesktopWindows함수의 프로시저 함수인 EnumWindowsProc을 살펴봅시다. 이 함수는 데스크탑과 관련된 모든 윈도우를 전부 열거해주는 기능을 하며 TRUE를 반환할 경우 다음 윈도우를 열거하고, FALSE를 반환하면 다음 윈도우를 열거하지 않고 바로 종료합니다. 다만 윈도우를 전부 열거하였다면 자동으로 FALSE를 반환합니다.

- DWORD PID,PID2;

프로세스의 ID를 저장할 변수 2개를 선언합니다.

- if(GetWindowTextLength(hWnd) == 0)

return TRUE;

윈도우의 타이틀이 없을 경우 TRUE를 반환하여 다음 윈도우로 넘어가도록 합니다.

- if(GetParent(hWnd) != NULL )

return TRUE;

윈도우가 부모윈도우를 가지고 있는 경우 즉 자식윈도우인 경우 TRUE를 반환하여 다음 윈도우로 넘어가도록 합니다.

- if(IsWindowVisible(hWnd) == FALSE)

return TRUE;

윈도우가 WS_VISIBLE 스타일을 갖지 않는 경우 TRUE를 반환하여 다음 윈도우로 넘어가도록 합니다.

- GetWindowThreadProcessId(Global_MainHWND,&PID);

작업관리자(메인 대화상자)핸들로부터 프로세스 ID를 가져와 PID에 저장시킵니다.

- GetWindowThreadProcessId(hWnd,&PID2);

EnumWindowsProc에서 열거한 윈도우의 프로세스 ID를 가져와 PID2에 저장시킵니다.

- if(PID == PID2)

return TRUE;

두 프로세스 ID값이 같으면 TRUE를 반환하여 다음 윈도우로 넘어가도록 합니다. 이는 작업관리자의 리스트뷰에 자기 자신의 윈도우를 추가하지 않도록 하기 위함입니다.

- Global_DesktopWND_title[Global_hwnd_count] = (TCHAR *)malloc(sizeof(TCHAR)*(GetWindowTextLength(hWnd)+1));

Global_DesktopWND_title배열을 현재 윈도우 타이틀의 크기만큼 동적할당합니다.

- memset(Global_DesktopWND_title[Global_hwnd_count],0,sizeof(TCHAR)*(GetWindowTextLength(hWnd)+1));

동적할당된 배열을 초기화시킵니다.

- GetWindowText(hWnd, Global_DesktopWND_title[Global_hwnd_count], 256);

윈도우의 타이틀을 256자만큼 갖고와 Global_DesktopWND_title에 저장시킵니다.

- Global_DesktopHWND[Global_hwnd_count] = hWnd;

윈도우의 핸들을 전역변수 배열에 저장시킵니다.

- Global_hwnd_count++;

저장이 완료되면 핸들의 카운트를 하나 증가시킵니다.

- if(checkDesktopWND_title(Global_DesktopWND_title[Global_hwnd_count-1]))

checkDesktopWND_title함수를 사용하여 Global_DesktopWND_title에 저장된 문자열이 리스트뷰에 추가되도 되는지 검사합니다. 만약 추가되지 않아야 하는 문자열이라면 참을 반환하고 아래의 코드를 수행합니다.

- free(Global_DesktopWND_title[Global_hwnd_count-1]);

부적절한 문자열이 들어간 인덱스 배열을 해제합니다.

- Global_hwnd_count--;

윈도우 핸들의 카운트도 하나 감소시킵니다.

- return TRUE;

다음 윈도우를 검사하기 위해 TRUE를 반환합니다.

- return TRUE;

하나의 윈도우가 정상적으로 처리가 되었다면 다음 윈도우를 열거하기 위해 TRUE를 반환합니다.

여기까지가 제가 작성한 작업관리자의 코드입니다. 물론 구현하지 않은 부분이 굉장히 많이 있고 자잘한 오류가 있을 수도 있습니다. 이에 대한 수정코드는 나중에 블로그에 포스팅하겠습니다. 결과를 한 번 살펴봅시다.

왼쪽이 제가 작성한 작업관리자, 오른쪽이 오리지널 작업관리자입니다. 자기 자신의 윈도우는 가져오지 않고 서로 작업관리자 윈도우를 가져옵니다.

위치를 Z축의 가장 높은 곳으로 설정하여 다른 윈도우와는 다르게 상태표시줄보다도 높은 곳에 위치하는 것을 볼 수 있습니다.

마지막으로 정렬도 제대로 되는 것을 확인할 수 있습니다.

네 이것으로 작업관리자 포스팅을 마치겠습니다. 따로 궁금하신 점이 있으시다면 댓글이나 방명록에 올려주십시요. 성실히 답변해드리겠습니다.

Posted by englishmath
,

안녕하십니까 이번 포스팅에서는 작업관리자에서 사용된 함수들을 살펴보겠습니다.

- void InsertItemDlglist(HWND *DesktopHWND1, TCHAR **DesktopWND_title1, int count);

리스트뷰에 항목을 추가하는 함수입니다. 인자는 데스크탑 핸들의 배열, 각 핸들의 타이틀명이 담긴 배열, 핸들의 개수 입니다.

- int checkDesktopWND_title(TCHAR *DesktopWND_title1);

데스크탑 핸들의 타이틀 명을 체크하는 함수입니다. 리스트 뷰에 추가되지 않은 핸들을 제외하기 위해 사용합니다. 인자는 해당하는 핸들의 타이틀 명입니다.

- int CMP_DesktopWND_title(void);

데스크탑 핸들의 타이틀을 비교하는 함수입니다. 윈도우의 타이틀 명이 변경되었을 때 리스트 뷰에 목록을 다시 추가하기 위해 사용합니다. 전역변수가 쓰이므로 인자는 받지 않습니다.

- void free_DesktopWND_title(void);

 핸들의 타이틀 명을 동적할당하여 저장하였으므로 이를 해제하기 위해 사용합니다.

- void free_DesktopWND_title2(void);

 변경되기 전의 윈도우 타이틀명이 저장된 배열을 해제하기 위해 사용합니다.

- void LVHeaderSort(HWND hWnd,TCHAR *DlgListHeader_Str[],int columnindex);

오리지널 작업관리자에서는 리스트 뷰 헤더 버튼을 누를시 작은 삼각형 아이콘이 생김으로써 오름차순인지 내림차순인지를 확인시켜줍니다. 이를 구현하기 위해 만든 함수입니다.

- void asce_sort(void);

리스트 뷰의 아이템들을 오름차순으로 보여주기 위해 데스크탑의 핸들과 타이틀명이 저장된 배열을 오름차순으로 정렬하는 함수입니다.

- void desc_sort(void);

리스트 뷰의 아이템들을 내림차순으로 보여주기 위해 데스크탑의 핸들과 타이틀명이 저장된 배열을 내림차순으로 정렬하는 함수입니다.

- void GetLVHeaderSort(void);

리스트뷰의 헤더가 오름차순 상태인지 아니면 내림차순 상태인지를 읽어와 정렬 함수를 호출하는 함수입니다.

- void MoveWND(HWND hWnd);

작업관리자의 크기가 변경되었을 경우 이에 맞게 컨트롤들의 위치와 크기를 변경해주는 함수입니다.

자 이제 하나씩 살펴봅시다.

- void InsertItemDlglist(HWND *DesktopHWND1, TCHAR **DesktopWND_title1, int count);

코드를 한 줄씩 살펴봅시다.

- int i;

for문을 사용하기 위해 선언하였습니다.

- HICON a;

핸들에서 추출한 아이콘을 저장할 변수 a를 선언하였습니다.

- LVITEM DlgList = {0};

리스트뷰에 항목을 추가할 때 필요한 변수인 LVITEM형 DlgList변수를 선언하고 초기화하였습니다. LVITEM구조체를 살펴봅시다.

여기서 쓰인 멤버들만 살펴봅시다.

* UINT mask

어떠한 멤버를 유효멤버로 정할지를 값으로 받는 멤버입니다. 이 값에 따라 유효멤버가 바뀌며 특정 멤버는 유효멤버가 아니면 어떤 값이 들어가든 무효가 되버립니다.

* int iImage

이미지리스트의 인덱스를 값으로 받습니다. 이 값에 따라 리스트 뷰의 항목에 추가되는 아이콘이 달라집니다.

* int iItem

리스트 뷰의 항목의 인덱스를 값으로 받습니다.

* int iSubItem

리스트 뷰의 하위항목의 인덱스를 값으로 받습니다. 리스트 뷰에서 하위항목이란 열을 의미합니다. 즉 열이 (작업, 상태) 이렇게 되어있으며 하위항목의 값이 각각 0,1로 설정됩니다.

* LPTSTR pszText

항목의 문자열을 받는 멤버입니다.

정리하면 이 구조체는 리스트 뷰에 항목을 추가하기 위해 사용하는 구조체입니다.

- SendDlgItemMessage(Global_TabDlgHWND,Tab1_DLG_List,LVM_DELETEALLITEMS,0,0);

Tab1_DIALOG 대화상자의 핸들이 저장된 지역변수 Global_TabDlgHWND에 속한 Tab1_DLG_List(리스트 뷰)에 LVM_DELETEALLITEMS메세지를 보내 리스트 뷰의 항목을 전부 삭제합니다.

- ImageList_Remove(Global_ImageList,-1);

이미지리스트인 Global_ImageList에 속한 이미지를 전부 삭제합니다. 

- for(i=0;i<count;i++)

핸들 개수만큼 아래의 코드를 반복합니다.

a = (HICON)SendMessage(DesktopHWND1[i],WM_GETICON,ICON_SMALL,0);

데스크탑의 핸들에 WM_GETICON메세지를 보냅니다. 인자가 ICON_SMALL이므로 작은 아이콘을 가져오게 됩니다. 반환된 작은 아이콘은 16*16크기이며 이를 a에 저장시킵니다.

- if(a == NULL)

a = (HICON)GetClassLongPtr(DesktopHWND1[i],GCLP_HICONSM);

만약 위의 SendMessage함수로 아이콘을 가져오지 못하였다면 GetClassLongPtr함수를 사용하여 아이콘을 가져옵니다. 

GetClassLongPtr함수는 지정된 핸들의 WNDCLASSEX 구조체에서 값을 가져오는 작업을 해주는 함수로 옵션을 GCLP_HICONSM로 주면 16*16크기의 아이콘을 가져옵니다. 아이콘을 가져오면 핸들을 a에 저장합니다.

- if(a == NULL)

a = LoadIcon(Global_hInstance,MAKEINTRESOURCE(IDI_ICON2));

GetClassLongPtr함수로도 아이콘을 가져오지 못한다면 리소스에서 대체아이콘을 가져와 a에 저장합니다.

- ImageList_AddIcon(Global_ImageList,a);

가져온 아이콘을 ImageList_AddIcon함수를 사용하여 Global_ImageList에 추가시킵니다.

- SendDlgItemMessage(Global_TabDlgHWND,Tab1_DLG_List,LVM_SETIMAGELIST,(WPARAM)LVSIL_SMALL,(LPARAM)Global_ImageList);

이미지가 저장된 Global_ImageList를 LVM_SETIMAGELIST메세지를 이용하여 Tab1_DLG_List 리스트 뷰에 등록시킵니다. 네번째 인자인 LVSIL_SMALL는 작은 아이콘 이미지 리스트를 의미합니다.

- Global_DesktopWND_title2[i] = (TCHAR *)malloc(sizeof(TCHAR)*(lstrlen(DesktopWND_title1[i])+1));

전역변수인 Global_DesktopWND_title2[i]에 핸들 타이틀명이 저장된 DesktopWND_title1[i] 크기만큼 동적할당합니다.

- memset(Global_DesktopWND_title2[i],0,sizeof(TCHAR)*(lstrlen(DesktopWND_title1[i])+1));

  lstrcpy(Global_DesktopWND_title2[i],DesktopWND_title1[i]);

동적할당된 배열을 초기화시켜주고 DesktopWND_title1[i]의 내용을 Global_DesktopWND_title2[i]에 복사시킵니다.

이러한 작업을 해주는 이유는 나중에 윈도우 타이틀의 문자열이 변경되었을 때 변경된 문자열과 변경 전 문자열을 비교해주기 위함입니다.

- DlgList.mask = LVIF_TEXT | LVIF_IMAGE;

LVITEM구조체인 DlgList의 mask멤버에 LVIF_TEXT | LVIF_IMAGE 값을 넣습니다. LVIF_TEXT는 pszText멤버를 유효화시키며 LVIF_IMAGE는 iImage멤버를 유효화시킵니다.

- DlgList.iImage = i;

가져올 이미지의 인덱스를 i로 지정합니다. 이렇게 하면 for문이 진행될때마다 이미지리스트에서 차례대로 아이콘을 가져옵니다.

- DlgList.pszText = DesktopWND_title1[i];

DesktopWND_title1[i]에 저장된 문자열을 pszText멤버에 대입합니다.

- DlgList.iItem = i;

항목의 인덱스를 i로 지정합니다. 이렇게 하면 for문이 진행될때마다 0~핸들개수 만큼차례대로 삽입하게 됩니다.

- DlgList.iSubItem = 0;

iSubItem값을 0으로 줌으로써 첫번째 하위항목(작업)에 삽입할 것을 명시합니다.

- SendDlgItemMessage(Global_TabDlgHWND,Tab1_DLG_List,LVM_INSERTITEM,0,(LPARAM)&DlgList);

Tab1_DLG_List 리스트 뷰에 LVM_INSERTITEM메세지를 보내 DlgList구조체에 담긴 내용을 토대로 항목을 추가합니다.

- DlgList.iImage = -1;

항목 삽입이 완료되었으면 iImage값에 -1을 주어 다음 항목을 삽입할 때 아이콘을 추가하지 않도록 합니다.

- DlgList.pszText = L"실행 중";

문자열을 바꿉니다.

  DlgList.iSubItem = 1;

iSubItem값을 1로 줌으로써 두번째 하위항목(상태)에 삽입할 것을 명시합니다.

- SendDlgItemMessage(Global_TabDlgHWND,Tab1_DLG_List,LVM_SETITEM,0,(LPARAM)&DlgList);

Tab1_DLG_List 리스트 뷰에 LVM_INSERTITEM메세지를 보내 DlgList구조체에 담긴 내용을 토대로 항목을 추가합니다.

- Global_Dlglist_count = i;

for문의 처리가 다 완료되었으면 i의 값을 Global_Dlglist_count에 저장시킵니다. 그러면이 Global_Dlglist_count는 리스트뷰에 추가한 항목의 수를 의미하게 됩니다.

다음 함수를 살펴봅시다.

- int checkDesktopWND_title(TCHAR *DesktopWND_title1);

- if(!lstrcmp(DesktopWND_title1,L"Program Manager"))

return 1;

인자로 받은 DesktopWND_title1이 Program Manager 인 경우 1을 반환합니다. 1을 반환한다는 것은 리스트 뷰에서 제외될 핸들이라는 뜻입니다.

- if(!lstrcmp(DesktopWND_title1,L"시작 메뉴"))

return 1;

인자로 받은 DesktopWND_title1이 시작 메뉴 인 경우 1을 반환합니다. 우리가 윈도우즈에서 시작 버튼을 눌렀을 때 보여지는 시작 메뉴도 하나의 핸들로 인식하므로 리스트 뷰에서 제외하기 위해 작성하였습니다.

- return 0;

핸들의 타이틀이 위에 해당되지 않는다면 0을 반환합니다.

- int CMP_DesktopWND_title(void);

- for(i=0;i<Global_hwnd_count;i++)  

데스크탑 핸들의 개수만큼 아래의 코드를 반복합니다.

- if(lstrcmp(Global_DesktopWND_title[i],Global_DesktopWND_title2[i])) 

free_DesktopWND_title2();

return 1;

리스트뷰에 추가될 때 저장된 핸들의 타이틀과 새 데스크탑 핸들의 타이틀을 검사합니다. 만약 두 타이틀의 내용이 다르다면 리스트뷰의 항목을 수정해야하므로 free_DesktopWND_title2함수를 호출하여 리스트뷰에 추가될 때 저장된 핸들을 해제해준 다음 1을 리턴합니다. 이 함수에서 1을 리턴하게 되면 InsertItemDlglist함수를 호출합니다.

- return 0;

타이틀의 비교과정에서 바뀐것이 없다면 0을 반환합니다.

- void free_DesktopWND_title(void)

- for(i=0;i<Global_hwnd_count;i++)        

free(Global_DesktopWND_title[i]);

 핸들의 타이틀이 저장된 Global_DesktopWND_title배열을 해제해줍니다.

- void free_DesktopWND_title2(void)

- for(i=0;i<Global_Dlglist_count;i++)        

free(Global_DesktopWND_title2[i]);

리스트뷰에서 항목을 추가할 때 저장시킨 핸들의 타이틀 배열을 해제합니다.

- void LVHeaderSort(HWND hWnd,TCHAR *DlgListHeader_Str[],int columnindex)

- HWND LVHeader;

리스트뷰의 헤더 핸들을 저장할 변수 LVHeader를 선언합니다.

- HDITEM HeaderItem;

HDITEM 구조체 변수 HeaderItem를 선언합니다. HDITEM을 살펴봅시다.

맥락은 LVITEM 구조체랑 비슷합니다. 쓰인 멤버들을 살펴봅시다.

* UINT mask

어떠한 멤버를 유효멤버로 정할지를 값으로 받는 멤버입니다. 이 값에 따라 유효멤버가 바뀌며 특정 멤버는 유효멤버가 아니면 어떤 값이 들어가든 무효가 되버립니다.

* int fmt

리스트뷰 헤더의 형식을 값으로 받는 멤버입니다.

* LPTSTR pszText

리스트뷰 헤더의 문자열을 값으로 받는 멤버입니다.

정리하면 이 구조체는 리스트 뷰 헤더를 조작할 때 사용합니다.

- LVHeader =(HWND)SendDlgItemMessage(hWnd,Tab1_DLG_List,LVM_GETHEADER,0,0);

Tab1_DLG_List 리스트뷰에 LVM_GETHEADER메세지를 보내 리스트뷰 헤더의 핸들을 얻어와 LVHeader에 저장시킵니다.

- SendMessage(LVHeader,HDM_GETITEM,columnindex,(LPARAM)&HeaderItem);

인자로 받은 값인 columnindex에 해당하는 헤더의 정보를 HDM_GETITEM메세지를 이용하여 얻어옵니다. 이렇게 얻어온 헤더의 정보는 HeaderItem구조체에 저장됩니다. 여기서 인자로 받은 columnindex는 사용자가 클릭한 헤더의 인덱스를 의미합니다. 

if(HeaderItem.fmt == 17408)

fmt의 값이 17408인 경우 아래의 코드를 수행합니다. 17408이란 값은 헤더의 상태가 오름차순을 가리킨다는 뜻입니다.

- HeaderItem.mask = HDI_TEXT | HDI_FORMAT;

mask멤버에 HDI_TEXT | HDI_FORMAT 값을 넣습니다. HDI_TEXT는 pszText멤버를 유효화시키며 HDI_FORMAT는 fmt멤버를 유효화시킵니다.

- HeaderItem.fmt = HDF_SORTDOWN | HDF_STRING;

fmt멤버에 HDF_SORTDOWN | HDF_STRING 값을 넣습니다. HDF_SORTDOWN는 헤더에 역삼각형 표시를 그리라는 의미(내림차순)이며 HDF_STRING은 항목에 문자열이 표시된다는 의미입니다.

- HeaderItem.pszText = DlgListHeader_Str[columnindex];

pszText멤버에 인자로 받은 DlgListHeader_Str배열의 columnindex번째 문자열을 저장시킵니다. DlgListHeader_Str배열은 헤더의 문자열이 저장되어 있는 배열을 의미하며 0번째에는 작업, 1번째에는 상태가 저장되어있습니다.

- else if(HeaderItem.fmt == 16896)

fmt가 16896일 경우 아래의 코드를 수행합니다. 16896이란 값은 헤더의 상태가 내림차순을 가리킨다는 뜻입니다.

- HeaderItem.mask = HDI_TEXT | HDI_FORMAT;

  HeaderItem.fmt = HDF_SORTUP | HDF_STRING;

  HeaderItem.pszText = DlgListHeader_Str[columnindex];

fmt속성 중 HDF_SORTDOWN가 HDF_SORTUP으로 바뀐 것 외에는 위와 동일합니다.

HDF_SORTUP은 삼각형 표시를 그리라는 의미(오름차순)입니다.

- else

fmt가 17408도 아니고 16896도 아니면 아래의 코드를 수행합니다. 이 말은 헤더가 오름차순이나 내림차순을 가리키지 않는다는 것을 의미합니다.

- HeaderItem.mask = HDI_TEXT | HDI_FORMAT;

  HeaderItem.fmt = 16384;

HeaderItem.fmt의 값에 16384로 지정합니다. 16384라는 값은 헤더가 오름차순이나 내림차순을 가리키지 않는다는 것을 의미합니다.(삼각형이 안그려진 헤더)

- if(columnindex == 0)

HeaderItem.pszText = DlgListHeader_Str[1];

SendMessage(LVHeader,HDM_SETITEM,1,(LPARAM)&HeaderItem);

columnindex 가 0인 경우 즉 0번째 헤더(작업)를 클릭한 경우입니다. 삼각형이 그려져 있지 않은 헤더를 눌렀을 경우 pszText멤버에  상태 헤더의 문자열을 저장합니다. 그리고 SendMessage함수로 1번째 헤더(상태)에 HDM_SETITEM메세지를 보내 헤더의 형식을 수정합니다.

- else

HeaderItem.pszText = DlgListHeader_Str[0];

     SendMessage(LVHeader,HDM_SETITEM,0,(LPARAM)&HeaderItem);

columnindex가 1인 경우 pszText에 작업 헤더의 문자열을 저장한 후 SendMessage함수로 0번째 헤더(작업)에 HDM_SETITEM메세지를 보내 헤더의 형식을 수정합니다.

이렇게 하면 삼각형이 그려져 있지 않은 헤더를 눌렀을 경우 누르지 않은 헤더에 있던 삼각형의 이미지가 사라집니다.

- HeaderItem.fmt = HDF_SORTUP | HDF_STRING;

  HeaderItem.pszText = DlgListHeader_Str[columnindex];

fmt에 오름차순 삼각형을 그리라는 HDF_SORTUP값을 넣고 누른 헤더의 문자열을 가져와 pszText에 저장시킵니다.

- SendMessage(LVHeader,HDM_SETITEM,columnindex,(LPARAM)&HeaderItem);

모든 if,else구문에 의해 HeaderItem의 멤버가 정리되었다면 SendMessage함수를 사용하여 columnindex헤더를 수정합니다.

- void asce_sort(void)

- int i,j;

for문에 사용할 변수 i,j를 선언합니다.

- TCHAR temp[256] = L"";

정렬을 위해 문자열을 임시로 저장할 변수 temp를 선언과 동시에 초기화합니다. 

- HWND temp2;

정렬을 위해 핸들을 임시로 저장할 변수 temp2를 선언합니다.

- for(i=0;i<Global_hwnd_count-1;i++)

서로 문자열을 비교하기 위해 for문을 돌립니다. 0부터 시작하되 배열의 인덱스개수보다 하나 낮게 최대값을 잡습니다.

- for(j=i+1;j<Global_hwnd_count;j++)

두번째 for문을 돌립니다. j는 i보다 하나 큰 값으로 시작하되 배열의 인덱스 개수를 최대값으로 잡습니다.

for문을 이렇게 선언한 이유는 배열[0] 배열[1] -> 배열[0] 배열[2] -> 배열[0] 배열[3] 이런 식으로 검사를 하기 위해서입니다.

- if((lstrcmp(Global_DesktopWND_title[i],Global_DesktopWND_title[j])) > 0)

Global_DesktopWND_title의 i번째 문자열과 j번째 문자열을 lstrcmp함수로 비교합니다. 이때 lstrcmp함수의 반환값이 양수일 경우 아래의 코드를 실행합니다. 반환값이 양수라는 것은 i번째 문자열이 j번째 문자열보다 값이 크다는 것을 의미합니다. 

예를 들어 i번째 문자열이 "나"이고 j번째 문자열이 "가"이면 lstrcmp함수는 양수를 반환한다는 뜻입니다.

- lstrcpy(temp,Global_DesktopWND_title[i]);

  free(Global_DesktopWND_title[i]);

i번째에 담긴 문자열을 temp에 옮기고 동적할당된 i번째 배열을 해제합니다.

- Global_DesktopWND_title[i] = (TCHAR *)malloc(sizeof(TCHAR)*(lstrlen(Global_DesktopWND_title[j])+1));

해제된 i번째 배열을 j번째 문자열 크기에 맞게 동적할당시킵니다.

- lstrcpy(Global_DesktopWND_title[i],Global_DesktopWND_title[j]);

j번째 문자열을 동적할당된 i번째로 옮깁니다.

- free(Global_DesktopWND_title[j]);

  Global_DesktopWND_title[j] = (TCHAR *)malloc(sizeof(TCHAR)*(lstrlen(temp)+1));

j번째 배열을 해제한 다음 i번째 문자열이 들어있는 temp 크기에 맞게 동적할당합니다.

- lstrcpy(Global_DesktopWND_title[j],temp);

j번째 배열에 i번째 문자열을 넣습니다.

이렇게 하면 i의 문자열과 j의 문자열 위치가 서로 바뀌게 됩니다. 예를 들어 [0]이 "나"이고 [1]이 "가" 였다면 위의 코드로 인해 [0]이 "가" 가 되고 [1]이 "나"가 된것이지요.

- temp2 = Global_DesktopHWND[i];

  Global_DesktopHWND[i] = Global_DesktopHWND[j];

  Global_DesktopHWND[j] = temp2;

문자열의 위치를 바꾸었으면 그에 맞는 핸들도 똑같이 위치를 바꿔줍니다.  

최종적으로 for문이 종료되면 문자열배열은 오름차순으로 정렬되어 집니다.

- void desc_sort(void)

내림차순이므로 lstrcmp함수가 음수를 반환할 때 코드를 수행합니다. 그 외에는 asce_sort함수와 동일합니다.

- void GetLVHeaderSort(void)

- HWND LVHeader;

  HDITEM HeaderItem0,HeaderItem1;

헤더의 정보를 얻기 위해 헤더의 핸들을 받는 변수와 HDITEM 구조체 변수 2개를 선언합니다.

- LVHeader =(HWND)SendDlgItemMessage(Global_TabDlgHWND,Tab1_DLG_List,LVM_GETHEADER,0,0);

Tab1_DLG_List 리스트뷰에 LVM_GETHEADER메세지를 보내 헤더의 핸들을 얻어와 변수에 저장시킵니다.

- SendMessage(LVHeader,HDM_GETITEM,0,(LPARAM)&HeaderItem0);

  SendMessage(LVHeader,HDM_GETITEM,1,(LPARAM)&HeaderItem1);

받아온 핸들을 이용해 0번째 헤더(작업)와 1번째 헤더(상태)에 HDM_GETITEM메세지를 보내 헤더의 상태를 가져옵니다.

- if(HeaderItem0.fmt == 17408 || HeaderItem1.fmt == 17408)

  asce_sort();

  else

desc_sort();

작업 헤더나 상태 헤더에 오름차순을 뜻하는 삼각형이 그려저 있을 경우 asce_sort() 함수를 호출하여 문자열과 핸들을 오름차순으로 정렬시킵니다.

그 외에는 작업 헤더나 상태 헤더에 내림차순을 뜻하는 역삼각형이 그려져 있다는 뜻이므로 desc_sort() 함수를 호출하여 문자열과 핸들을 내림차순으로 정렬시킵니다.

- void MoveWND(HWND hWnd)

- int i;

현재 포커스를 가지고 있는 탭 컨트롤의 탭 인덱스를 가져오기 위한 변수 i를 선언하였습니다.

- RECT WNDRT;

핸들의 영역을 받아오기 위해 RECT형 변수를 선언하였습니다.

- GetClientRect(hWnd,&WNDRT);

인자로 받은 hWnd의 영역을 구해와 변수에 저장시킵니다. 여기서 hWnd는 사용자가 크기조정이 가능한 메인 대화상자의 핸들입니다.

-    MoveWindow(GetDlgItem(hWnd,Tab1),WNDRT.left,WNDRT.top,WNDRT.right,WNDRT.bottom-30,TRUE);

Tab컨트롤의 핸들을 가져와 메인 대화상자에 크기에 맞게 위치와 크기를 재조정합니다.     

-    MoveWindow(Global_TabDlgHWND,WNDRT.left,WNDRT.top+30,WNDRT.right,WNDRT.bottom-70,TRUE);

리스트 뷰가 들어있는 대화상자를 가져와 메인 대화상자에 크기에 맞게 위치와 크기를 재조정합니다. 

-    MoveWindow(GetDlgItem(Global_TabDlgHWND,Tab1_DLG_List),WNDRT.left+20,WNDRT.top+10,WNDRT.right-35,WNDRT.bottom-115,TRUE);

리스트뷰의 핸들을 가져와 메인 대화상자에 크기에 맞게 위치와 크기를 재조정합니다. 

- MoveWindow(GetDlgItem(Global_TabDlgHWND,TaskExit),WNDRT.right-338,WNDRT.bottom-95,102,24,TRUE);

  MoveWindow(GetDlgItem(Global_TabDlgHWND,change),WNDRT.right-230,WNDRT.bottom-95,102,24,TRUE);

  MoveWindow(GetDlgItem(Global_TabDlgHWND,NewTask),WNDRT.right-122,WNDRT.bottom-95,102,24,TRUE);

대화상자의 작업 끝내기, 전환, 새작업 버튼을 대화상자에 크기에 맞게 위치를 재조정합니다. 오리지널 작업관리자에서 이 세개의 버튼은 대화상자의 크기가 바껴도 위치만 바뀔뿐 크기가 바뀌진 않으므로 MoveWindow의 right와 bottom의 인자를 고정값으로 주었습니다.

- MoveWindow(GetDlgItem(hWnd,static1),WNDRT.left+5,WNDRT.bottom-16,70,12,TRUE);

  MoveWindow(GetDlgItem(hWnd,static2),WNDRT.left+100,WNDRT.bottom-16,70,12,TRUE);

  MoveWindow(GetDlgItem(hWnd,static3),WNDRT.left+220,WNDRT.bottom-16,70,12,TRUE);

메인 대화상자의 스태틱들을 대화상자에 크기에 맞게 위치를 재조정합니다. 이 스태틱들은 크기가 고정되어 있으며 메인 대화상자의 bottom값에 따라 y위치가 바뀌므로 인자를 저렇게 주었습니다.

- i = SendDlgItemMessage(hWnd,Tab1,TCM_GETCURFOCUS,0,0);

모든 윈도우의 위치변경이 끝났으면 Tab컨트롤에 TCM_GETCURFOCUS메세지를 보내 현재 선택된 탭 인덱스를 가져와 i에 저장합니다.

- switch(i)

case 0:

ShowWindow(Global_TabDlgHWND,SW_HIDE);

ShowWindow(Global_TabDlgHWND,SW_SHOW);

break;

default:

break;

이 인덱스의 값이 0(응용프로그램)이면 ShowWindow의 SW_HIDE로 윈도우를 한번 숨긴다음 ShowWindow의 SW_SHOW로 윈도우를 다시 보여줍니다. 그냥 SW_SHOW옵션만 쓰면 크기가 변경되었을 때 컨트롤들이 보이지 않게 됩니다. 이 코드 덕분에 크기를 변경할 때 살짝 매끄럽지 못할 수 있습니다.

그 외의 탭이 선택되었다면 변경된 컨트롤들을 보여주지 않습니다.

자 이것으로 함수설명을 마치겠습니다. 다음 포스팅에선 작업관리자에 쓰인 코드를 보여드리겠습니다.



Posted by englishmath
,

안녕하십니까 이번 포스팅에서는 작업관리자의 리소소를 보여드리겠습니다. 액셀러레이터는 틀만 만들고 구현을 하지 않았기 때문에 생략하였습니다.

먼저 대화상자를 보겠습니다.

작업관리자의 메인 대화상자가 되는 부분입니다. 대화상자 안에는 탭 컨트롤을 추가하였고 밑의 글자들은 스태틱으로 작성하였습니다.

메인 대화상자의 첫번째 탭에서 동작할 대화상자입니다. 대화상자 안에는 리스트 뷰와 버튼을 추가하였습니다. 그리고 대화상자의 visible속성을 FALSE로 주어 리스트뷰와 버튼만 보이도록 하였습니다. 마지막으로 이 대화상자를 자식 윈도우로 지정하여 메인 대화상자가 종료될 때 같이 종료되도록 하였습니다.

이제 아이콘을 살펴봅시다.

작업관리자의 메인 아이콘입니다. 이 아이콘은 오리지널 작업관리자에서 추출하였습니다.

작업관리자의 리스트뷰에 추가될 아이콘입니다. 이 아이콘은 데스크탑 핸들에서 아이콘을 추출하지 못했을 경우에 쓰는 대체아이콘이라고 보시면 됩니다.

마지막으로 메뉴를 살펴봅시다.

이 메뉴는 응용프로그램 탭에서 동작하는 메뉴입니다. 다른 탭에서는 이 메뉴와 다른 메뉴가 나타납니다.

이것으로 리소스 포스팅이 끝났습니다. 다음 포스팅에서는 코드에 쓰인 함수들을 살펴보겠습니다.


Posted by englishmath
,

안녕하십니까? 이번 포스팅에서는 제가 구현한 작업관리자의 알고리즘을 설명드리겠습니다. 

물론 실력이 부족하여 전부 구현하지는 못하였고 최소한의 기능(응용프로그램)만 구현하였습니다. 그럼 한번 알고리즘을 살펴봅시다.

1. 작업관리자는 윈도우가 아닌 대화상자로 생성되고 대화상자에 메뉴, 탭 컨트롤 등을 추가한다.

2. 작업관리자 창의 응용프로그램 부분은 리스트 뷰로 구현되어 있다.

3. 리스트 뷰에는 데스크탑에서 얻어온 윈도우 핸들이 추가된다.

   이 때 가져오는 윈도우 핸들은 제목표시줄에 문자열이 존재해야 하고 자식윈도우가 아니어야 하며 보여지는 윈도우(WS_VISIBLE)이어야 한다.

4. 리스트 뷰의 헤더 버튼을 누르면 가져온 윈도우의 핸들들이 정렬된다.

5. 작업관리자는 항상 최상의 창 위치에 존재한다.

6. 작업끝내기 버튼을 누를시 해당되는 핸들에 WM_CLOSE 메세지를 보낸다.

7. 전환 버튼을 누를 시 작업관리자 창은 최소화가 되고 포커스를 해당되는 핸들로 전환한다.

8. 작업관리자는 자기 자신의 핸들을 리스트 뷰에 추가시키지 않는다.

등이 있겠네요. 물론 실제로는 더 많이 있겠지만 일단 제가 구현한 부분만 설명드렸습니다. 다음 포스팅에서는 리소스를 한 번 살펴봅시다.


Posted by englishmath
,

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

- #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
,

안녕하십니까 이번 포스팅에서는 메모장을 작성할 때 쓰인 함수들을 살펴보겠습니다.

쓰인 함수는 다음과 같습니다.

- void SaveFile_as(HWND hWnd) : 공통 대화 상자를 호출하여 파일을 저장할 때 쓰이는 함수입니다.

- void SaveFile(HWND hWnd) :  공통 대화 상자를 호출하지 않고 파일을 저장할 때 쓰이는 함수입니다.

- void FileOpen(HWND hWnd) : 공통 대화 상자를 호출하여 파일을 열 때 쓰이는 함수입니다.

- void SaveDlgBox(HWND hWnd) : 파일을 작성하다가 다른 작업을 수행할 때 작성된 파일의 저장여부를 묻는 작업을 하는 함수입니다.

먼저 SaveFile_as 함수를 살펴봅시다. 매개변수는 메모장의 핸들입니다.(edit 윈도우 핸들이 아닙니다)

하나씩 설명해 드리겠습니다.

- OPENFILENAME Save;

OPENFILENAME 구조체 변수인 Save를 선언합니다. OPENFILENAME 구조체를 살펴봅시다.

이 구조체는 파일 열기 대화상자와 파일 저장 대화상자를 열기 위해 사용하는 구조체입니다. 멤버 변수가 굉장히 많은데 이중 쓰인 멤버 변수만 설명하겠습니다.

* DWORD lStructSize

이 구조체의 크기를 받는 멤버변수 입니다. 필수로 입력해줘야 합니다.

* HWND hwndOwner

호출되는 공통대화상자를 소유하는 윈도우의 핸들을 받는 멤버변수 입니다. 이 값이 없으면 메모장과 공통대화상자가 따로 놀게 됩니다.

* LPCTSTR lpstrFilter

공통대화상자를 사용할 때의 필터 형식을 받는 멤버변수입니다. 즉

이 부분을 의미합니다. 보시다시피 텍스트 문서와 모든 파일, 총 2개의 필터로 되어있는 것을 확인할 수 있습니다. 이 필터 형식은 \0를 이용하여 작성하는데 이는 나중에 추가로 설명하겠습니다.

* LPTSTR lpstrFile

공통 대화 상자에서 사용자가 작성한 파일 이름과 경로를 저장할 배열을 받는 멤버변수입니다. 배열에 값이 들어있으면 처음 공통 대화상자를 호출할 때 그 값이 파일 이름으로 지정되어 있습니다. 메모장에서는 *.txt가 초기값으로 적혀있습니다.

* DWORD nMaxFile

lpstrFile멤버 변수에 저장된 배열의 크기를 받는 멤버변수입니다.

* DWORD Flags

호출하는 대화상자의 속성을 받는 멤버 변수 입니다. 속성은 여러가지가 있지만 여기서 쓰인 속성들만 알아 봅시다.

OFN_PATHMUSTEXIST : 사용자가 유효하지 않은 파일 이름을 입력했을 경우 경고 메세지 박스를 표시합니다.

OFN_OVERWRITEPROMPT : 파일을 저장할 때 동일한 파일이 있을 경우 사용자에게 덮어쓸건지의 여부를 묻는 메세지 박스를 표시합니다.

* LPCTSTR lpstrDefExt

사용자가 파일 이름에 확장자를 입력하지 않았을 때 자동으로 쓰일 확장자를 멤버변수로 받습니다. 메모장에서는 *.txt가 기본 확장자로 지정되어있습니다.

자 이제 설명이 끝났으니 다음 코드를 살펴봅시다.

- TCHAR name[256] = L"*.txt",notepad_title[261];

공통 대화 상자에서 사용자가 입력한 파일 이름을 저장시킬 name변수와 메모장의 타이틀 부분에 표시할 문자열을 저장하기 위한 notepad_title변수를 선언합니다.

- HANDLE file,filename;

새로운 파일을 만들기 위한 핸들형 file변수와 만든 파일에서 경로명을 제외한 파일명을 추출하기 위한 핸들형 filename을 선언합니다. 참고로 이 HANDLE은 HWND하고는다릅니다. HWND는 윈도우의 핸들을 저장하는 용도이고 HANDLE은 파일의 핸들을 저장하는 용도로 쓰입니다.

- LRESULT editstr_len;

메모장의 edit 윈도우에 작성된 내용의 길이를 저장시키기 위한 변수 editstr_len을 선언합니다. 자료형이 LRESULT인 이유는 edit 윈도우에 작성된 내용의 길이를 받기 위해 사용하는 SendMessage함수의 반환값이 LRESULT이기 때문입니다.

- LPTSTR Wide_str;

edit 윈도우에 작성된 문자열(유니코드)을 저장하기 위해서 선언하였습니다.

- char *mult_str;

유니코드 문자열을 멀티바이트로 변환한 후 변환된 멀티바이트 문자열을 저장하기 위해 선언하였습니다.

- WIN32_FIND_DATA WFD;

WIN32_FIND_DATA구조체 변수인 WFD를 선언합니다. MSDN에서 WIN32_FIND_DATA구조체를 한 번 알아봅시다.

정리하면 WIN32_FIND_DATA구조체는 FindFirstFile, FindFirstFileEx, FindNextFile 함수로 찾은 파일의 정보를 저장할 때 쓰이는 구조체라고 되어 있네요. 여기서 이 구조체를 쓴 이유는 저장한 파일의 파일명을 추출하기 위해서입니다. 메모장에서 파일을 저장시키면 메모장의 제목표시줄 부분이 저장된 파일 명으로 바뀌지요? 즉 메모장의 제목표시줄 내용을 바꾸기 위해 사용되었다고 보시면 됩니다.

여기서 멤버변수는 딱 하나 사용하였습니다. 사용한 멤버변수만 설명드리겠습니다.

*TCHAR cFileName

찾은 파일의 파일명을 저장하는 멤버변수입니다.

- int str_len;

유니코드 문자열을 멀티바이트 문자열로 변환시킬 때 필요한 멀티바이트 문자열의 길이를 받기 위해 선언하였습니다. 유니코드 문자열을 멀티바이트 문자열로 변환시키기 위해선 변환될 멀티바이트 문자열의 길이가 필요한데 이 길이를 받기 위한 변수입니다.

- memset(&Save,0,sizeof(Save));

처음에 선언한 OPENFILENAME구조체의 Save변수는 현재 각 멤버값이 쓰레기값으로 설정되어 있으므로 원활한 사용을 위해 Save변수의 각 멤버들을 memset함수를 사용하여 전부 0으로 초기화 시킵니다. memset함수의 정의를 한 번 살펴봅시다.

지정한 버퍼를 지정한 값으로 설정한다고 나와있습니다. 인수는 총 3개가 있습니다만 간단하니 간략하게 설명만 하고 넘어가겠습니다.

dest는 버퍼의 주소, c는 버퍼에 설정할 값, count는 버퍼의 사이즈를 받습니다. 간단하지요? 다음으로 넘어갑시다.

- Save.lStructSize = sizeof(Save);

lStructSize멤버 변수에 Save 구조체의 크기를 넣어줍니다. 이 부분은 필수로 해줘야 합니다.

- Save.hwndOwner = hWnd;

hwndOwner에 메모장의 핸들값을 넣어 메모장이 공통대화상자를 소유하도록 하였습니다.

- Save.lpstrFilter = L"텍스트 문서(*.txt)\0*.txt\0모든 파일 (*.*) \0*.*";

lpstrFilter에 공통대화상자에서 쓰일 필터형식을 넣습니다. 처음의 문자열은 첫번째 필터목록에 적혀있는 글자를 적었구요. 그 다음은 \0으로 구분을 한후 이 필터를 선택할 시에 보여질 확장자를 입력합니다. 텍스트 문서이니 당연히 *.txt 확장자를 입력해야겠지요? 그다음 다시 \0으로 구분을 한 후 두번째 필터에 적혀있는 글자를 적고 모든 파일을 의미하는 확장자인 *.*을 입력하였습니다.

- Save.lpstrFile = name;

파일의 이름과 경로를 저장시킬 배열(name)을 lpstrFile멤버 변수에 집어넣습니다.

- Save.nMaxFile = sizeof(name);

name의 배열크기를 넣어주었습니다.

- Save.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT;

앞서 설명드린 2개의 옵션을 넣어주었습니다.

- Save.lpstrDefExt = L"*.txt";

메모장에선 대부분 txt파일을 사용하므로 기본 확장자를 txt로 지정해주었습니다.

- if(GetSaveFileName(&Save) != 0)

GetSaveFileName함수의 결과값이 참일 경우 if문을 수행합니다. GetSaveFileName함수를 한 번 알아봅시다.

파일을 저장하는 공용대화상자를 호출하는 함수라고 되어있네요. 인자는 OPENFILENAME 구조체의 주소입니다. 호출한 대화상자에서 사용자가 이름을 입력하고 확인 버튼을 누르면 반환값은 0이 아니구요. 그 외의 오류나 혹은 사용자가 취소를 눌렀을 경우 반환값은 0입니다. 이 함수가 정상적으로 수행이 되면 Save구조체 멤버변수에 각각의 값이 들어가집니다.

즉 이 if문은 사용자가 파일 이름을 입력하고 확인 버튼을 눌러 파일을 저장하려고 할 때 수행합니다.

-  wsprintf(FILEPATH,L"%s",Save.lpstrFile);

wsprintf함수를 사용하여 TCHAR 형 FILEPATH 전역변수에 Save.lpstrFile의 값을 %s형식으로 저장합니다. 이 함수를 수행하고 나면 FILEPATH에는 저장한 파일의 경로가 들어가게 됩니다. 이러한 작업을 거치는 이유는 이 문서가 저장이 되지 않은 새로운 문서인지 아니면 저장된 문서를 불러와 수정하는 문서인지를 구별하기 위해서입니다. 이는 나중에 자세히 알려드리겠습니다.

- file = CreateFile(Save.lpstrFile,GENERIC_READ | GENERIC_WRITE,NULL,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

CreateFile함수를 사용하여 파일을 만들고 그 파일의 핸들을 file변수에 저장시키는 작업을 하는 부분입니다. CreateFile함수를 자세히 살펴봅시다.

설명을 보면 파일 혹은 입출력 장치를 열거나 만드는 함수라고 되어있습니다. 인자들을 하나씩 살펴봅시다.

* LPCTSTR lpFileName

열거나 만들 파일의 경로명입니다. 여기서는 GetSaveFileName함수에 의해 지정된 경로명인 Save.lpstrFile의 값이 들어갔습니다.

* DWORD dwDesiredAccess

파일의 권한을 설정하는 부분입니다. 파일을 읽기전용으로 작업할려면  GENERIC_READ, 쓰기 전용으로 작업할려면 GENERIC_WRITE, 읽고 쓰기로 작업할려면 두 옵션을 전부 넣어주면 됩니다. 여기서는 두 옵션을 전부 넣었습니다.

* DWORD dwShareMode

파일의 공유모드를 설정하는 부분인데 지금은 딱히 신경쓸 필요가 없으므로 NULL값을 줍시다.

* LPSECURITY_ATTRIBUTES lpSecurityAttributes

파일의 보안과 핸들 상속에 관해 설정하는 부분인데 역시 지금은 신경쓸 필요가 없으므로 NULL값을 주었습니다.

* DWORD dwCreationDisposition

파일을 열건지 만들건지에 대해 설정하는 부분입니다. 여기는 GetSaveFileName함수가 성공했을 때 실행하는 부분이기 때문에 파일을 무조건 만들거나 혹은 이미 있는 파일에 덮어쓰기를 해야 하므로 CREATE_ALWAYS옵션을 주었습니다.

* DWORD dwFlagsAndAttributes

파일의 속성을 지정하는 부분입니다. 딱히 다른 속성을 넣을 필요성을 못느낀다면 일반적으로 FILE_ATTRIBUTE_NORMA를 줍니다.

* HANDLE                hTemplateFile

파일의 확장속성을 제공하는 템플릿 파일의 핸들을 받습니다. 솔직히 이부분은 무슨 뜻인지 몰라 그냥 NULL값을 주었습니다.

다음으로 넘어갑시다.

- editstr_len = SendMessage(Edit,WM_GETTEXTLENGTH,0,0);

SendMessage함수를 사용하여 Edit 윈도우에 WM_GETTEXTLENGTH메세지를 보내고 그 결과값을 editstr_len에 저장합니다. WM_GETTEXTLENGTH메세지는 윈도우와 연관된 텍스트의 길이를 반환해달라고 요청하는 메세지이며 반환되는 값은 NULL문자를 포함하지 않은 문자열의 길이입니다.

-  Wide_str = (TCHAR *)malloc(sizeof(TCHAR)*(editstr_len+1));

malloc함수를 이용해 Wide_str배열을 editstr_len의 길이에 맞게 동적할당합니다. 원래 malloc함수의 기본 반환값은 void 포인터 이지만 대부분 특정 포인터를 반환하기 위해 형변환을 많이 사용합니다. Edit윈도우의 문자는 TCHAR이므로 TCHAR의 size * (문자열의 길이 + NULL) 만큼 할당하고 Wide_str의 자료형은 TCHAR이므로 TCHAR 포인터로 반환합니다.

- mult_str = (CHAR *)malloc(sizeof(TCHAR)*(editstr_len+1));

마찬가지로 malloc함수를 이용해 mult_str배열도 editstr_len의 길이에 맞게 동적할당합니다. 다만 mult_str은 멀티바이트 문자열을 저장하기 위한 변수이므로 반환할 떄에는 CHAR 포인터로 반환합니다.

- memset(Wide_str,0,sizeof(TCHAR)*(editstr_len+1));
  memset(mult_str,0,sizeof(TCHAR)*(editstr_len+1));

할당된 두 배열들을 0으로 초기화 시켜 줍니다. 참고로 여기서 주의 하실 점이 있는데 동적할당된 배열을 memset으로 초기화 시킬 때에는 sizeof(배열)이 아닌 동적할당 할때의 그 size를 주어야 합니다.
  
- GetWindowText(Edit,Wide_str,editstr_len+1);

배열의 준비가 다 되었으니 이제 문자열을 긁어올 때입니다. GetWindowText함수를 사용하여 Edit윈도우에 적혀있는 텍스트를 널까지 포함하여 긁어온 후 Wide_str에 저장시킵니다.

- str_len = WideCharToMultiByte(949,NULL,Wide_str,lstrlen(Wide_str),NULL,NULL,NULL,NULL);

str_len 변수에 WideCharToMultiByte함수의 결과값을 넣습니다. 이 코드는 WideCharToMultiByte함수를 사용하여 Wide_str에 저장되어 있는 유니코드 문자열을 멀티바이트 문자열로 바꾸기 위한 첫번째 작업입니다. WideCharToMultiByte함수를 사용하기 위해선 멀티바이트 문자열의 길이가 필요합니다만 아직 멀티바이트로 변환도 되지 않은 상태에서 유니코드의 문자열만으로 멀티바이트 문자열의 길이를 계산하기엔 조금 복잡합니다. 그러므로 WideCharToMultiByte함수의 반환값이 멀티바이트 문자열의 길이라는 것을 이용하여 str_len변수에 길이를 저장시키는 것입니다.

WideCharToMultiByte함수의 정의를 한 번 봅시다.

UTF-16(유니코드) 문자열을 새로운 문자열로 변환시키는 함수라고 되어있군요. 인자들을 하나씩 살펴봅시다.

* UINT CodePage

문자열을 변환할 때 기준이 되는 코드 페이지를 인자로 받습니다. 여기서는 지정된 인자를 쓰지 않았는데 이유는 지정된 인자로 문자열을 변환시켜 저장한 파일을 일반 메모장에서 불러오면 글자가 깨져버리기 때문입니다. 그래서 저는 일반 메모장을 리버싱하여 메모장에서 WideCharToMultiByte함수를 사용할 때 쓰인 인자인 949를 넣어주었습니다. 이 949라는 값은 윈도우즈에서 사용하는 일반적인 한글을 뜻합니다.

* DWORD dwFlags

변환 타입을 지정하는 부분입니다. 이 부분은 잘 몰라서 NULL값을 주었습니다.

* LPCWSTR lpWideCharStr

바꿀 유니코드 문자열입니다.

* int cchWideChar

유니코드 문자열의 길이입니다.

* LPSTR lpMultiByteStr

코드페이지에 의해 변환된 멀티바이트 문자열을 저장할 배열을 인수로 받습니다. 하지만 처음 쓰인 WideCharToMultiByte함수의 이 인자는 NULL값인데 그 이유는 목적이 문자열 변환이 아닌 변환된 문자열의 길이를 얻기 위해서입니다. 즉 쓰일 필요가 없어서 그냥 NULL값을 주었습니다.

* int cbMultiByte

멀티바이트의 문자열의 길이를 인자로 넣습니다. 역시 첫 WideCharToMultiByte함수는 이 값을 알아내기 위해 사용하는 것이므로 이 인자또한 NULL로 주었습니다.

* LPCSTR lpDefaultChar

유니코드 문자열 중 지정한 코드페이지로 변환이 불가능한 문자가 있을 경우 이 인자에 지정된 문자열로 변환합니다. 사용하지 않을 경우에는 NULL을 줍니다.

* LPBOOL lpUsedDefaultChar

유니코드 문자열 중 지정한 코드페이지로 변환이 불가능한 문자가 있어 lpDefaultChar의 값으로 변환이 된 경우는 TRUE, 아니면 FALSE 값을 저장시킬 BOOL형 변수의 포인터를 인자로 받습니다. 역시 사용하지 않을 경우에는 NULL을 줍니다.

마지막으로 이 함수의 반환값은 변환된 멀티바이트 문자열의 길이입니다. 즉 우리는 이 코드를 통해 멀티바이트로 변환된 문자열의 길이를 알아내어 str_len변수에 저장시킵니다.

- WideCharToMultiByte(949,NULL,Wide_str,lstrlen(Wide_str),mult_str,str_len,NULL,NULL);

앞 WideCharToMultiByte함수로 알아낸 멀티바이트로 변환된 문자열의 길이를 이용하여 실제로 유니코드 문자열을 멀티바이트 문자열로 변환시키는 작업을 하는 코드입니다. 인자에 mult_str과 str_len이 들어가신 것을 볼 수 있습니다.

- WriteFile(file,mult_str,strlen(mult_str),(LPDWORD)&str_len,NULL);

CreateFile함수로 생성된 파일에 mult_str에 들은 문자열을 쓰는 부분입니다. WriteFile함수를 살펴봅시다.

지정된 파일 혹은 입출력 장치에 데이터를 기록해주는 함수라고 되어있습니다. 인자들을 살펴봅시다.

* HANDLE hFile

기록할 파일의 핸들입니다.

* LPCVOID lpBuffer

파일에 기록될 데이터가 들어있는 버퍼입니다.

* DWORD nNumberOfBytesToWrite

기록될 데이터의 길이입니다.

* LPDWORD lpNumberOfBytesWritten

작성된 데이터의 바이트 수를 저장시킬 변수의 포인터를 인자로 받습니다. 여기서는 str_len의 주소를 인자로 주었습니다. 다만 자료형이 LPDWORD이므로 형변환을 해주었습니다.

* LPOVERLAPPED lpOverlapped

OVERLAPPED구조체의 포인터를 인자로 받습니다. 이 부분은 잘 모르므로 그냥 NULL값을 주었습니다.

즉 이 WriteFile함수를 사용하여 파일에 멀티바이트로 변환된 데이터를 작성합니다.즉 여기까지 수행하면 저장이 완료된 것입니다. 그럼 이제 마무리가 남았군요. 한 번 봅시다.
 
- filename = FindFirstFile(Save.lpstrFile,&WFD);

FindFirstFile함수를 이용하여 Save.lpstrFile 경로에 있는 파일을 찾아 그 파일의 핸들을 filename에 저장합니다. FindFirstFile함수를 살펴봅시다.

지정한 파일을 찾아 열고 그 파일의 핸들을 반환하는 함수입니다. 인자들을 하나씩 살펴봅시다.

* LPCTSTR lpFileName

찾을 파일의 경로를 인자로 받습니다.

* LPWIN32_FIND_DATA lpFindFileData

WIN32_FIND_DATA 구조체의 주소를 인자로 받습니다. 파일을 찾는데 성공하면 이 구조체에 파일의 정보가 들어가게 됩니다.

즉 정리하면 이 코드는 CreateFile함수로 만든 파일을 찾아서 그 정보를 WIN32_FIND_DATA 구조체인 WFD의 각 멤버에 저장시킵니다. 이러한 작업을 거치는 이유는 파일의 이름을 추출하기 위해서이지요. 파일의 이름을 추출하는 이유는 메모장의 제목표시줄의 내용을 수정하기 위해서인데 Save.lpstrFile을 쓰면 경로까지 다 포함되기 때문에 우리는 파일명만 추출하기 위해 FindFirstFile함수를 사용한 것입니다.

이 코드가 실행하고 나면  WFD의 cFileName멤버에 파일명(확장자 포함)이 들어가게 됩니다.

- memset(notepad_title,0,sizeof(TCHAR)*(lstrlen(WFD.cFileName)+1));

notepad_title 변수를 파일명의 길이+NULL값 만큼만 초기화를 시켜줍니다.

- wsprintf(notepad_title,L"%s - 메모장",WFD.cFileName);

notepad_title변수에 WFD.cFileName의 값을 "%s - 메모장" 형식으로 넣습니다. 이 코드가 수행되면 notepad_title에는 파일이름.txt - 메모장 이런 식으로 값이 들어가게 됩니다.

- SetWindowText(hWnd,notepad_title);

SetWindowText함수를 사용하여 메모장의 제목표시줄을 notepad_title의 값으로 수정합니다. 이 SetWindowText함수는 보통 인자로 받은 핸들의 제목표시줄을 수정할 때 쓰입니다.

- free(Wide_str);
  free(mult_str);

모든 작업이 끝났으므로 동적할당한 배열들을 해제시킵니다.
  
- FindClose(filename);
  CloseHandle(file);

  
모든 작업이 끝났으므로 열려있는 파일의 핸들들을 전부 닫습니다. 

- str_CHANGE = 0;

성공적으로 저장이 끝나면 str_CHANGE 전역변수의 값을 0으로 줍니다. 이 str_CHANGE 변수는 문자열이 변경됬는지 변경 안됬는지의 여부를 저장시키는 변수입니다. 메모장에서는 저장이 되지 않은 파일을 닫기 버튼을 눌렀을 경우 edit윈도우에 적힌 글자수가 0이나 아니냐에 의해 변경된 내용을 저장시키는 박스가 뜹니다. 허나 파일을 한 번 저장한 후 추가 작업을 하거나 불러온 파일을 작업하는 경우에는 문자열의 길이에 상관없이 텍스트가 살짝이라도 변경되면 박스가 뜹니다.

좀더 쉽게 얘기하자면 12345라는 값이 저장된 텍스트를 불러온후 바로 닫으면 박스가 뜨지 않지만 12345라는 값에 6을 적어 123456으로 만든 후 다시 6을 지워 12345로 만들었을 경우에는 원본 파일이랑 값이 똑같지만 저장을 묻는 박스가 뜨게 됩니다.

- else
  CANCEL = 1;

사용자가 공용대화상자에서 저장버튼을 누르지 않고 취소버튼을 눌렀을 때의 처리입니다. 이때는 CANCEL 전역 변수의 값을 1로 설정합니다. 이렇게 하는 이유는 예를 들어 설명드리겠습니다.

메모장에서 내용을 작성하다가 닫기 버튼을 누르면 변경 내용을 저장하겠습니까? 라는 창이 뜨지요? 그 창에는 저장, 저장 안함, 취소 버튼이 있는데 저장 버튼을 누를 경우 경우에 따라 대화상자를 띄워서 저장을 하거나 아니면 대화상자를 띄우지 않고 저장을 한 후 메모장이 종료됩니다. 반대로 저장 안함을 누르면 파일을 저장하지 않고 종료가 됩니다. 그러나 마지막으로 남은 취소 버튼을 누르면 종료가 되지 않습니다. 즉 종료 알고리즘을 제대로 수행할려면 이 취소 버튼을 눌렀느냐 누르지 않았느냐의 여부를 체크해야 하는데 이를 위해 선언한 변수가 CANCEL입니다.

그런데 이 CANCEL변수가 파일을 저장하는 것과 무슨 관계가 있을 까요? 정답은 사용자가 호출된 공용대화상자에서 취소 버튼을 눌러 저장을 하지 않았을 때 메모장이 종료되지 않는다는 것입니다. 즉 저장, 저장 안함, 취소 버튼 중 저장 버튼을 눌렀음에도 불구하고 사용자가 저장을 하지 않으면 메모장이 종료되지 않기 때문에 이를 구현하기 위해 사용하였습니다. 나중에 좀 더 자세히 설명드리겠습니다.

후 길고 긴 SaveFile_as함수 설명이 끝났습니다. 이 함수는 보통 메모장에서 새 파일을 저장할 때나 혹은 다른 이름으로 저장 버튼을 눌렀을 때 쓰입니다.

다음으로 넘어가 SaveFile함수를 설명드리겠습니다. 매개변수는 SaveFile_as랑 동일합니다. 이 함수는 대화상자를 호출한다는 점을 제외하면 SaveFile_as 함수랑 똑같은 기능을 수행합니다. 그러므로 SaveFile_as에서 설명한 코드는 따로 설명하지 않겠습니다.

 - GetWindowText(hWnd,notepad_title,261);

일반적으로 GetWindowText함수는 edit윈도우의 내용을 불러오는 데 쓰이지만 일반 윈도우 핸들을 인자로 받는 GetWindowText함수는 윈도우의 제목표시줄의 값을 읽어옵니다. 여기서는 현재 작성중인 문서가 새로 작성하는 문서인지 혹은 이미 저장된 파일을 읽어와 수정하는 문서인지를 판별하기 위해 사용하였습니다.

새로 작성하는 문서라면 제목표시줄의 값은 제목 없음 - 메모장 일 것이고 수정하는 문서라면 파일이름.txt - 메모장 일 것입니다. 그리고 이 값에 따라 저장한는 방법이 달라지게 됩니다.

정리하면 메모장의 제목표시줄에서 261자만큼 문자열을 들고와 notepad_title에 저장시킵니다.

- if(lstrcmp(notepad_title,L"제목 없음 - 메모장") == 0)
   SaveFile_as(hWnd);

lstrcmp함수를 이용하여 notepad_title의 문자열과 "제목 없음 - 메모장" 문자열을 비교합니다. 만약 두 문자열의 값이 같다면 이 함수는 0을 반환하고 아니면 0이 아닌 값을 반환합니다.

이 함수가 0을 반환하면 메모장의 제목표시줄의 내용은 제목 없음 - 메모장 이라는 뜻이고 이는 곧 이 파일이 새로 작성하는 문서라는 것을 알 수 있습니다. 그러므로 새로 작성하는 문서를 저장할 때에는 공용대화상자를 호출하여 저장하기 위해 SaveFile_as(hWnd) 함수를 호출하여 처리합니다.

- else 

아닌 경우에는 대화상자를 호출하지 않고 파일 저장 작업을 시도합니다. 이 다음 코드는 SaveFile_as함수 코드랑 비슷하니 설명을 생략하겠습니다.

이제 다음 함수인 FileOpen함수를 살펴봅시다. 말 그대로 파일을 여는 작업을 하는 함수를 뜻하며 파일을 열때에는 공용대화상자를 무조건 호출하므로 파일을 저장하는 작업과는 다르게 함수가 하나입니다. 아까랑 마찬가지로 역할이 비슷한 코드는 생략하겠습니다.

- OPENFILENAME Open;

공용대화상자를 호출하기 위해 OPENFILENAME 구조체 변수인 Open을 선언합니다.

- TCHAR name[256] = L"",notepad_title[261];

name과 notepad_title변수를 선언합니다. 다만 파일을 저장할때하고는 다르게 열때에는 초기값이 따로 지정되어 있지 않으므로 그냥 ""로 초기화시켰습니다.

- int filesize,str_len;

저장된 파일의 크기를 읽어올 변수인 filesize와 MultiByteToWideChar함수에서 유니코드 문자열 길이를 저장시킬 변수인 str_len변수를 선언하였습니다. 같은 자료형이므로 변수를 하나로 선언해서 써도 되지만 가독성을 위해 일부러 2개의 변수로 선언하였습니다.

-DWORD size;

ReadFile함수에서 쓰일 DWORD형 size변수를 선언하였습니다.

Open구조체 멤버의 초기화 부분은 SaveFile_as함수와 동일하므로 생략하겠습니다.

- if(GetOpenFileName(&Open) != 0)

GetOpenFileName함수를 이용하여 파일 열기 대화 상자를 호출합니다. 그 외는 GetSaveFileName함수와 동일합니다. 파일 열기 버튼을 누른다면 아래의 코드를 수행하게 됩니다.
 
- wsprintf(FILEPATH,L"%s",Open.lpstrFile);

 FILEPATH전역 변수에 Open.lpstrFile값을 %s형식으로 저장시킵니다. 앞에서도 이야기 했지만 이 FILEPATH 변수는 이 파일이 새로 작업하는 파일인지 아니면 수정된 파일인지를 체크하는 부분입니다. 이 FILEPATH변수는 메모장의 타이틀 부분이 바뀔 때마다 값이 달라집니다.

- file = CreateFile(Open.lpstrFile,GENERIC_READ | GENERIC_WRITE,NULL,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

CreateFile함수를 이용하여 파일을 엽니다. 앞의 함수에서는 파일을 만드는 옵션인 CREATE_ALWAYS옵션을 썻다면 여기서는 파일을 여는 옵션인 OPEN_ALWAYS옵션을 사용합니다.

- filesize = GetFileSize(file,NULL);

CreateFile함수를 이용해 연 파일의 핸들로부터 파일 크기를 받아옵니다. 앞의 SaveFile_as함수처럼 Edit윈도우에서 크기를 받아올 수 없으므로 GetFileSize함수를 사용하여 파일크기를 구합니다. GetFileSize함수를 한 번 알아봅시다.

파일 크기를 바이트 단위로 반환해주는 함수입니다. 인자를 한 번 살펴봅시다.

* HANDLE hFile

대상 파일의 핸들입니다.

* LPDWORD lpFileSizeHigh

파일의 크기가 클 경우 사용하는 인자입니다. 지금은 일단 필요없으니 NULL값을 줍시다.

다음으로 넘어갑시다.

- mult_str = (CHAR *)malloc(sizeof(TCHAR)*(filesize+1));
  Wide_str = (TCHAR *)malloc(sizeof(TCHAR)*(filesize+1));

앞서 구한 filesize의 값을 이용하여 각 배열들을 동적할당합니다.

- memset(mult_str,0,sizeof(TCHAR)*(filesize+1));
  memset(Wide_str,0,sizeof(TCHAR)*(filesize+1));

동적할당 후 초기화 시켜줍시다.
  
- ReadFile(file,mult_str,filesize,&size,NULL);

ReadFile함수를 사용하여 파일을 읽습니다. 이 ReadFile함수를 살펴봅시다.

지정된 파일에서 데이터를 읽는 함수입니다. 인자들을 한 번 살펴봅시다.

* HANDLE hFile,

대상 파일의 핸들입니다.

* LPVOID lpBuffer

읽은 데이터를 저장시킬 버퍼를 인자로 받습니다.

* DWORD nNumberOfBytesToRead

읽어올 최대 바이트 값을 인자로 받습니다.

* LPDWORD lpNumberOfBytesRead

읽은 바이트 수를 저장시킬 DWORD형 변수의 주소를 인자로 받습니다. 여기서는 size변수를 인자로 주었습니다.

*LPOVERLAPPED lpOverlapped

OVERLAPPED구조체 변수의 주소를 인자로 받습니다. 이 부분은 WriteFile함수와 동일하게 NULL값을 주었습니다.

- str_len = MultiByteToWideChar(949,NULL,mult_str,strlen(mult_str),NULL,NULL);

읽어들인 멀티바이트 코드를 유니코드로 변환시키기 위해 MultiByteToWideChar함수를 사용하여 변환될 유니코드 길이를 구한 후 str_len에 저장시킵니다. 이 MultiByteToWideChar함수를 살펴봅시다.

말 그대로 멀티바이트 코드를 UTF-16(유니코드)문자열로 바꿔주는 함수입니다. 인자들을 살펴봅시다.

* UINT CodePage

변환에 수행할 코드페이지입니다. 앞서 WideCharToMultiByte에서 949 값으로 변환을 했으니 여기서도 949값을 이용합니다.

* DWORD dwFlags

변환 타입을 지정하는 부분입니다. 이 부분은 WideCharToMultiByte와 동일하게 NULL값을 줍시다.

* LPCSTR lpMultiByteStr

바꿀려는 멀티바이트 문자열을 인자로 받습니다.

* int cbMultiByte

멀티바이트 문자열의 길이입니다.

* LPWSTR lpWideCharStr

변환된 유니코드 문자열을 저장할 배열을 인자로 받습니다.

*int cchWideChar

유니코드 문자열의 길이입니다.

이번에도 WideCharToMultiByte함수와 똑같이 유니코드 문자열 길이를 구하기 위해 첫 MultiByteToWideChar함수에는 마지막 인자 2개 값을 NULL로 주었습니다. 이렇게 구해진 유니코드 문자열의 길이는 str_len변수에 저장됩니다.

- MultiByteToWideChar(949,NULL,mult_str,strlen(mult_str),Wide_str,str_len);

 str_len의 값을 이용해 멀티바이트 문자열을 유니코드로 변환시켜 Wide_str변수에 저장합니다.
  
- SetWindowText(Edit,Wide_str);

Edit클래스의 내용을 Wide_str문자열로 설정합니다. 여기까지 코드가 수행되면 이제 파일 열기 작업은 다 끝난 것입니다. 그 다음 코드들은 메모장의 제목표시줄을 수정하고 동적할당 된 배열을 해제, 파일 핸들을 닫는 작업인데 이 부분은 SaveFile_as함수와 동일하므로 생략하겠습니다.

마지막으로 SaveDlgBox 함수를 설명하겠습니다. 역시 이 함수의 인자도 메모장의 핸들입니다. 이 함수는 작업 도중 메모장을 종료할 때 나오는 메세지 박스를 구현한 것입니다. 이 메세지 박스는 일반 MessageBox함수를 사용한 것이 아니기 때문에 따로 함수를 사용하여 구현하였습니다.

- TASKDIALOGCONFIG TDBOX;

TASKDIALOGCONFIG 구조체 변수인 TDBOX를 선언합니다. TASKDIALOGCONFIG의 정의를 한 번 봅시다.

이 구조체는 메세지 박스를 띄우기 위해 사용하는 구조체입니다. 다시 한 번 말하지만 이 메세지 박스는 MessageBox 함수가 띄우는 메세지 박스하고는 다릅니다.

여기서 사용된 멤버들만 한 번 알아봅시다.

* UINT cbSize;

구조체의 크기를 받는 멤버변수입니다.

* HWND hwndParent;

이 메세지 박스의 소유자 핸들을 받는 멤버변수입니다.

* HINSTANCE hInstance;

보통 WINAPI함수의 인자를 매개변수로 받습니다.

* TASKDIALOG_COMMON_BUTTON_FLAGS dwCommonButtons

이 멤버변수는 메세지박스에 표시할 버튼을 지정하는 부분입니다. 사용자가 지정할 버튼 외에 따로 추가할 버튼을 인자로 주시면 됩니다.

자료형이 TASKDIALOG_COMMON_BUTTON_FLAGS입니다. 한 번 살펴봅시다.

여러개의 플래그가 포함되어 있는 것을 확인할 수 있습니다. 이 6개의 플래그중 메세지 박스에 표시할 버튼을 골라 값을 주시면 됩니다.

여기서는 사용자 지정 버튼(저장, 저장 안함)을 제외한 취소 버튼을 추가하기 위해 TDCBF_CANCEL_BUTTON을 값으로 주었습니다.

 * PCWSTR pszWindowTitle;

메세지박스의 제목표시줄을 인자로 받는 부분입니다.

* const TASKDIALOG_BUTTON * pButtons;

TASKDIALOG_BUTTON 구조체의 주소를 인자를 받는 부분입니다. 이 부분이 바로 사용자가 정의한 버튼을 받는 부분이라고 볼 수 있습니다. TASKDIALOG_BUTTON 구조체를 한 번 살펴봅시다.

메세지 박스에 표시될 버튼의 정보를 저장하는 구조체입니다. 각 인자들을 살펴봅시다.

** int nButtonID

버튼이 선택되었을 때 반환될 값을 인자로 받습니다. 쉽게 말하면 버튼의 ID라고 보시면 됩니다.

** PCWSTR pszButtonText

버튼에 표시될 문자열을 인자로 받습니다.

만약 여러개의 버튼을 정의하고 싶으시다면 TASKDIALOG_BUTTON 구조체의 변수를 배열로 선언하시면 됩니다.

* UINT cButtons

pButtons의 배열의 항목 수입니다. 직접 입력해줘도 되지만 ARRAYSIZE함수를 사용해도 됩니다.

* PCWSTR pszMainInstruction

메세지 박스에 표시할 문자열을 인자로 받습니다.

자 다음으로 넘어갑시다.

- const TASKDIALOG_BUTTON TDBUTTON[] = { {IDOK, L"저장"},{IDNO, L"저장 안함"} };

pButtons 인자에 넣을 배열인 TASKDIALOG_BUTTON 구조체 변수인 TDBUTTON을 배열로 선언합니다. 그리고 선언된 배열에 값을 주어 IDOK라는 저장 버튼과 IDNO 라는 저장 안함 버튼을 만듭니다. IDOK과 IDNO는 매크로 상수를 의미하며 각각 OK버튼과 NO버튼을 의미합니다.

- LRESULT editstr_len;

SendMessage함수를 이용하여 문자열의 길이를 받아올 변수를 선언하였습니다.

- LPTSTR MessageStr;

메세지박스에 표시할 문자열을 저장할 변수 MessageStr을 선언하였습니다.

- int result = 0,str_len = 0;

메세지 박스에서 버튼을 눌렀을 때 그버튼의 ID값을 저장할 변수인 result와 SendMessage함수를 사용하지 않고 문자열의 길이를 저장할 변수 str_len 변수를 선언하였습니다.

- memset(&TDBOX,0,sizeof(TDBOX));

구조체를 초기화시킵니다.

- TDBOX.cbSize = sizeof(TDBOX);

cbSize멤버 변수에 구조체의 크기를 넣어줍니다.

- TDBOX.hwndParent = hWnd;

부모핸들을 메모장의 핸들로 지정합니다.

- TDBOX.hInstance = Global_hInstance;

hInstance를 WINAPI함수의 인자인 hInstance값을 넣어줍니다. Global_hInstance는 hInstance값을 쓰기 위한 전역변수입니다.

- TDBOX.dwCommonButtons = TDCBF_CANCEL_BUTTON;

사용자가 정의한 버튼 외에 취소 버튼을 추가하기 위해서 TDCBF_CANCEL_BUTTON 값을 넣어주었습니다.

- TDBOX.pszWindowTitle = L"메모장";

메세지박스의 제목표시줄을 메모장으로 설정합니다.

- TDBOX.pButtons = TDBUTTON;

정의한 버튼의 배열인 TDBUTTON을 인자로 집어넣어 줍니다.

- TDBOX.cButtons = ARRAYSIZE(TDBUTTON);

TDBUTTON 배열의 항목개수를 인자로 넣어줍니다. 2라는 값을 넣어도 되고 ARRAYSIZE함수를 이용하여 주어도 됩니다. ARRAYSIZE함수는 매개변수를 배열로 받아 배열의 요소 개수를 반환하는 함수입니다.
 
- if(lstrcmp(FILEPATH,L"제목 없음 - 메모장") == 0)

FILEPATH와 "제목 없음 - 메모장" 문자열을 비교합니다. 값이 같다면 지금 작성하고 있는 파일은 새로 작성하는 파일을 의미하며 값이 다르다면 저장된 파일을 수정하는 파일을 의미합니다. 새로 작성하는 파일일 경우 아래의 코드를 수행합니다.

-  editstr_len = SendMessage(Edit,WM_GETTEXTLENGTH,0,0);

Edit 윈도우로부터 문자열의 길이를 받습니다.

- if(editstr_len > 0)

Edit 윈도우에 적힌 문자가 1개 이상 있을 경우 아래의 코드를 수행합니다.
  
- TDBOX.pszMainInstruction = L"변경 내용을 제목 없음에 저장하시겠습니까?";

메세지박스의 내용을 "변경 내용을 제목 없음에 저장하시겠습니까?"로 지정합니다.

- TaskDialogIndirect(&TDBOX, &result, NULL, NULL);

TaskDialogIndirect함수를 이용하여 실질적으로 메세지박스를 호출합니다. TaskDialogIndirect함수를 살펴봅시다.

메세지 박스를 호출하는 함수입니다. 인자들을 살펴봅시다.

* const TASKDIALOGCONFIG *pTaskConfig

TASKDIALOGCONFIG 구조체 변수의 주소를 인자로 받습니다.

int *pnButton

메세지 박스에서 버튼을 눌렀을 때 그 버튼의 값(ID)을 저장하는 변수의 주소를 인자로 받습니다.

* int *pnRadioButton

메세지 박스에서 라디오 버튼을 눌렀을 때 그 버튼의 값(ID)을 저장하는 변수의 주소를 인자로 받습니다. 사용하지 않을 경우 NULL값을 줍니다.

* BOOL *pfVerificationFlagChecked

검증 체크 박스의 선택 여부를 인자로 받는 부분인데 아직은 우리랑 상관이 없으니 NULL값을 주었습니다.

- switch (result)

메세지박스에서 버튼을 눌렀을 때 그 버튼의 처리를 위한 switch문입니다. 

 - case IDOK:
     SaveFile(hWnd);
     break;
   case IDNO:
     CANCEL = 0;
     break;
  case IDCANCEL:
     CANCEL = 1;
     break;

IDOK(저장)버튼을 눌렀을 경우 SaveFile함수를 호출합니다. IDNO(저장안함) 버튼을 눌렀을 경우  CANCEL 변수를 0으로 설정합니다. IDCANCEL(취소) 버튼을 눌렀을 경우 취소버튼을 눌렀다는 것을 알리기 위해 CANCEL값을 1로 설정합니다.

- else
   CANCEL = 0;

만약 Edit 윈도우에 아무것도 적혀있지 않은 상태라면 메세지 박스를 호출하지 않고 CANCEL 값을 0으로 줍니다.

- else

이번 else는 메모장의 제목표시줄의 값이 "제목 없음 - 메모장"이 아닐 경우의 처리입니다. 즉 FILEPATH 변수의 값이 제목 없음 - 메모장이 아니라는 것이지요. 이럴 때는 아래의 코드를 수행합니다.

-  if(str_CHANGE == 1)

str_CHANGE 의 값이 1일 경우 아래의 코드를 수행합니다. 메모장의 제목표시줄 값이 제목 없음 - 메모장이 아닐 경우에는 지금 작업중인 파일이 한번 저장된 파일 혹은 불러온 파일 이라는 것을 의미합니다. 이런 경우에는 Edit 윈도우의 문자열 길이랑은 상관없이 문자열이 변경되었는지의 여부에 따라 메세지박스 호출 여부가 달라집니다.

정리하면 문자열이 변경되었을 경우 아래의 작업을 수행합니다.  
  
- str_len = lstrlen(FILEPATH);

FILEPATH 변수에 들어있는 문자열의 길이를 구하여 str_len변수에 저장합니다. 지금 FILEPATH변수에는 현재 작업 중인 파일의 경로가 들어있습니다.

- MessageStr = (TCHAR *)malloc(sizeof(TCHAR)*(str_len+19));

MessageStr변수를 str_len에 맞게 동적할당 해줍니다. 여기 보시면 str_len값에 19를 더해주는 부분이 있는데 이는 나중에 설명드리겠습니다.

- memset(MessageStr,0,sizeof(TCHAR)*(str_len+19));

동적할당한 변수를 초기화시킵니다.

wsprintf(MessageStr,L"변경 내용을 %s에 저장하시겠습니까?",FILEPATH);

MessageStr에 FILEPATH값을 "변경 내용을 %s에 저장하시겠습니까?" 형식으로 저장합니다. 여기 보시면 %s외에도 다른 문자열이 추가로 들어간 것을 볼 수 있습니다. %s를 제외한 문자열은 "변경 내용을 에 저장하시겠습니까?" 인데 이 문자열의 길이는 총 18입니다. 즉 앞의 동적할당에서 그냥 str_len의 길이만큼만 동적할당을 해버리면 MessageStr변수는 %s외의 추가적인 문자열을 저장하지 못하게 됩니다. 그러므로 str_len에 추가 문자열(18)과 NULL(1)을 추가로 더해주어 MessageStr이 모든 문자열을 저장할 수 있게 해주었습니다.

- TDBOX.pszMainInstruction = MessageStr;

이렇게 저장된 MessageStr의 값을 메세지박스의 내용으로 지정해줍니다.

- TaskDialogIndirect(&TDBOX, &result, NULL, NULL);

메세지 박스를 호출합니다.

- switch (result)
    case IDOK:
     SaveFile(hWnd);
     break;
    case IDNO:
     CANCEL = 0;
     break;
    case IDCANCEL:
     CANCEL = 1;
     break;

누른 버튼에 따라 처리를 해줍니다.

- free(MessageStr);

모든 작업이 다 끝났으니 동적할당한 배열을 해제합시다.

네 이것으로 메모장에서 쓰인 4개의 함수들의 설명을 마치겠습니다. 다음 포스팅에선 소스코드를 직접 보여드리겠습니다. 

Posted by englishmath
,

안녕하십니까 이번 포스팅에서는 메모장을 만들때 쓰인 리소스를 설명하겠습니다.

먼저 메뉴부분 리소스 입니다.

이 부분의 캡션을 자세히 보시면 파일(&F)라고 되어있는 것을 볼 수 있습니다. 여기서 &는 &다음에 오는 문자를 단축키로 만들겠다는 뜻입니다. 일반 메모장에서 alt+F를 누르면 파일 메뉴가 자동으로 선택되지요? 그것을 구현한 것입니다.

그외의 다른 메뉴도 이와 똑같이 작성하여 단축키를 구현하였습니다. 그리고 각 서브메뉴의 ID는 여러분 편한대로 지정해주시면 됩니다.

이번엔 아이콘 리소스를 봅시다.

이 아이콘 리소스는 메모장에서 쓰인 아이콘 리소스를 그대로 추출한 것입니다. 이 아이콘은 여타 쓰인 아이콘과는 조금 다른 아이콘인데 이 아이콘의 특징은 아이콘이 쓰이는 곳에 따라 크기와 모습이 달라지는 아이콘입니다. 실제로 메모장을 실행하면

작업표시줄에는 이런 모양의 아이콘이 보입니다. 하지만 메모장 프로그램을 살펴보면

위와 같이 작업표시줄에 뜨인 아이콘과는 다른 모습의 아이콘이 등장합니다. 즉 이를 구현하기 위해 메모장에 쓰인 아이콘을 그대로 추출하여 사용하였습니다.

이제 다이로그 리소스를 살펴봅시다.

이 부분은 페이지 설정이란 메뉴를 눌렀을 때 나타나는 페이지 설정 대화상자를 구현한 부분입니다. 용지,방향,여백,미리보기 부분은 GROUPBOX를 사용하였고 크기,공급 등의 글자는 static text를 사용하였으며 세로,가로는 radio button, 미리보기 안의 사진과 그림자 부분은 picture control, 크기와 공급을 설정하는 부분은 combobox, 여백값과 머리글,바닥글을 입력하는 부분은 edit control을 사용하였습니다.

이제 액셀러레이터 리소스를 살펴봅시다.

여기서 액셀러레이터라는 것은 단축키를 설정할 때 사용합니다. 단 아까와 같이 Alt를 눌러 사용하는 단축키가 아닌 독자적인 단축키를 설정할 때 사용합니다. 일반 메모장에서는 저장과 열기 등을 누를 때 Ctrl+S, Ctrl+O를 사용하지요? 이를 구현하기 위해 작성한 리소스입니다.

ID는 단축키를 눌렀을 때 동작될 메뉴의 ID를 의미합니다. Key Modifer은 단축키와 추가적으로 누를 키를 설정합니다. 여기서는 Ctrl로 설정하였습니다. 그리고 Key는 말그대로 단축키를 의미하며 Type은 ascii와 virtual key 둘 중 하나로 지정할 수가 있는데 Key Modifer기능을 사용할려면 virtual key로 지정해야 하기에 .virtual key로 지정하였습니다.

네 이것으로 리소스 설명을 마치겠습니다.

다음 포스팅에서는 메모장을 만들 때 쓰인 함수를 설명하겠습니다.

'Programing > 프로그램 직접 개발하기(C)' 카테고리의 다른 글

메모장 만들기 - 소스  (0) 2017.03.27
메모장 만들기 - 함수들  (0) 2017.03.18
메모장 만들기 - 알고리즘  (0) 2017.03.18
지뢰찾기 다듬기 1  (0) 2017.01.09
지뢰찾기 만들기 - 소스  (3) 2017.01.07
Posted by englishmath
,

안녕하십니까? 우선 포스팅이 늦어진 점 사과드립니다.

이번 포스팅에서는 윈도우즈에서 제공하는 메모장 프로그램을 한번 만들어보겠습니다. 물론 100퍼 똑같이 구현은 하지 못하지만 그래도 최대한 비슷하게 만들어보았습니다.

먼저 메모장의 알고리즘을 설명하자면

1. 메모장은 일반 윈도우와 Edit윈도우 2개로 이루어져 있다

2. Edit윈도우의 크기는 일반 윈도우의 크기에 따라 달라진다.

3. 메모장에 작성한 내용을 저장시킬 때에는 유니코드로 작성된 텍스트를 멀티바이트로 변환시켜 저장한다.

4. 반대로 텍스트 파일을 열 경우에는 텍스트를 멀티바이트로 읽어 유니코드로 변환시킨 후 Edit윈도우에 표시한다.

5. 파일을 열거나 저장할 때는 윈도우즈에서 제공하는 공통대화상자를 사용한다.

등이 있겠네요.

자 그럼 이제 다음 포스팅에서 본격적으로 시작해보겠습니다.

 

Posted by englishmath
,

안녕하십니까 이번 포스팅에선 저번 포스팅에서 만든 지뢰찾기에 기능을 추가해보겠습니다. 먼저 바뀐 소스코드를 봅시다.

바뀐 소스코드만 찍어서 올렸습니다. 하나씩 살펴봅시다.

- int time_value = 0;

지뢰찾기의 시간을 저장시키기 위한 변수를 선언하였습니다.

- BOOL start = FALSE;

BOOL형인 start변수를 선언하고 FALSE값으로 초기화 시켰습니다. 이 변수는 지뢰찾기를 시작했을 때 타이머가 작동하도록 하기 위해 쓰였습니다.

- int landmine_value = 10;

사용자가 우측 마우스를 눌러 지뢰임을 표시할 때 지뢰의 개수를 나타내기 위해 선언하였습니다.

- wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); -> 원본코드

 wndclass.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH); -> 수정코드

윈도우창의 배경색을 바꿔주었습니다. LTGRAY_BRUSH는 밝은 회색이란 뜻입니다.

이제 WndProc함수를 살펴봅시다.

- HBITMAP Number6,Number7,Number8,space; -> 원본코드

  HBITMAP Number6,Number7,Number8,space,landmine_count,timer; -> 수정코드

비트맵을 출력시키기 위해 HBITMAP형 변수 landmine_count와 timer를 선언하였습니다. 이 변수는 각각

비트맵 이미지를 저장시킵니다.

- RECT rt = {55,200,100,250};
  RECT rt2 = {150,200,200,250};

처음 보는 자료형인 RECT가 나왔습니다. 이 RECT의 정의를 한 번 봅시다.

구조체이군요. 각 멤버들은 왼쪽,높이,오른쪽,아래 를 나타냅니다. 이것이 무슨 말이냐면 사각형의 왼쪽 위 모서리 좌표와 오른쪽 아래 모서리 좌표를 저장시킨다는 뜻입니다. 즉 사각형 모양의 영역을 지정할 때 쓰이는 구조체입니다. 이 구조체를 선언하는 이유는 나중에 설명드리겠습니다.

아무튼 RECT의 구조체를 2개 선언하여 각각 초기값을 줍니다. 이러면 rt에는 (55,200)에서 (100,250) 까지의 사각형 영역을 나타내고 rt2에는 (150,200)에서 (200,250) 까지의 사각형 영역을 나타냅니다.

- TCHAR time_count[150]=L"";

지뢰찾기에서는 몇초가 지나갔다 혹은 지뢰 몇개가 남아있다는 것을 숫자로 사용자에게 보여주는데 이 값들은 시시각각 변합니다. 그러므로 %d형으로 값을 출력시켜야 합니다. 즉 wsprintf함수를 사용해야 합니다. 그래서 이 wsprintf함수를 사용하기 위해 TCHAR 형 변수를 선언하였습니다.

자 이제 WM_CONTEXTMENU를 살펴봅시다.

- if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 1)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(Flag)));
     flagTile[i] = 0;  -> 원본코드

  if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 1)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(Flag)));
     flagTile[i] = 0;
     landmine_value--;
     InvalidateRect(hWnd,&rt2,TRUE); -> 수정코드

수정코드를 잘 보시면 landmine_value--; 가 적혀있습니다. 오리지널 지뢰찾기에서는 사용자가 우측 마우스를 눌러 지뢰가 있다는 것을 표기하면 지뢰의 개수가 하나 줄어든 값을 보여줍니다.

이렇게 말이지요. 이것을 구현하기 위해 지뢰의 개수를 나타내는 변수인  landmine_value값을 하나 감소시킵니다. 이렇게 값을 하나 감소시키고 나면 감소시킨 값을 사용자에게 보여주어야 겠지요? 이를 위해 InvalidateRect함수를 사용하였습니다. 정의를 한 번 봅시다.

이 함수는 특정 영역을 무효화 영역으로 만들어주는 함수입니다. 앞 포스팅에서 설명하였지만 무효화 영역이 발생하면 윈도우에서는 WM_PAINT메세지가 호출됩니다. 즉 이 함수는 WM_PAINT메세지를 발생시키기 위해 사용하는 함수인데 WM_PAINT메세지를 발생시킴으로써 보여지는 landmine_value값을 감소시킨 landmine_value값으로 다시 그리는 것이지요. 인자들을 하나씩 살펴봅시다.

* hWnd

무효화 영역을 발생시킬 대상 핸들입니다.

* RECT *lpRect

RECT 구조체는 앞에서 설명드렸지요? 영역을 나타내는 구조체입니다. 즉 이 인자부분은 무효화시킬 영역을 의미합니다.

*BOOL bErase

이 인자는 영역을 무효화시킬 때 그 영역의 배경을 지우고 무효화 시키느냐 혹은 배경을 지우지 않고 무효화 시키느냐를 결정합니다. TRUE값으로 주면 배경을 지우고 무효화시키며 FALSE를 주면 배경을 지우지 않고 무효화 시킵니다. 특별한 상황이 아니면 대부분 TRUE값을 줍니다.

우리는  InvalidateRect(hWnd,&rt2,TRUE); 코드를 작성함으로써 rt2의 영역을 무효화시킵니다. 나중에 나오겠지만 이 rt2의 영역은 지뢰 개수를 나타내는 숫자 부분 영역입니다.  즉 이 영역을 무효화 시키고 WM_PAINT메세지를 호출하여 달라진 지뢰 개수 값으로 다시 그린다고 보시면 됩니다.

-else if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 0)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(QM)));
     flagTile[i] = 2; -> 원본코드

- else if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 0)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(QM)));
     flagTile[i] = 2;
     landmine_value++;
     InvalidateRect(hWnd,&rt2,TRUE); -> 수정코드

이 코드 부분은 사용자가 깃발이 있는 타일에 오른쪽 마우스를 눌러 ?타일로 바꾸는 부분입니다. 깃발타일이 사라졌으므로  landmine_value값을 하나 증가시키고 다시 그리기 위해 InvalidateRect함수를 호출합니다.

다음은 WM_COMMAND메세지를 봅시다.

- if(flagTile[wParam] == 1)
      DestroyWindow(buttonHwnd[wParam]);
      OpenClose_Tile[wParam] = 0;

- if(flagTile[wParam] == 1)
      if(!start)
          start = TRUE;
          SetTimer(hWnd,1,1000,NULL);
      DestroyWindow(buttonHwnd[wParam]);
      OpenClose_Tile[wParam] = 0;

버튼을 눌렀을 때의 코드입니다. 오리지널 지뢰찾기에서는 정상적인 버튼을 눌러서 버튼이 벗겨졌을 경우 타이머가 작동합니다. 우리도 그것을 구현하기 위해 버튼을 처음 눌렀을 경우 SetTimer함수를 호출하도록 코드를 작성하였습니다. start변수에는 FALSE가 초기값으로 들어있었고 FALSE의 부정은 TRUE이므로 버튼을 처음 눌렀다면 if(!start)에서 참으로 인식하여 SetTimer함수를 호출합니다. 그리고 start변수에 TRUE값을 주어 다음에 버튼을 누를 경우 if문이 실행되지 않도록 해줍니다.

setTimer함수를 한 번 살펴봅시다.

함수 명 그대로 타이머를 설정해주는 함수입니다. 인자를 살펴 봅시다.

*hWnd

타이머와 관련된 핸들입니다. 보통 WndProc의 인자인 핸들이 인자로 들어갑니다.

*nIDEvent

타이머의 이름을 의미하는 인자입니다. 1을 줄 경우 그 타이머의 이름은 1이 됩니다. 이렇게 지정된 타이머의 이름은 KillTimer함수를 사용할 때 쓰입니다.

*uElapse

타이머의 주기를 설정하는 부분입니다. 원래 이 SetTimer함수가 호출되고 난 후에는 일정 주기마다 WM_TIMER메세지가 발생하는데 그 일정주기를 설정해주는 부분입니다. 단위는 ms(밀리 세컨드)이기 때문에 값을 1000으로 주면 1초마다 WM_TIMER메세지가 발생하도록 할 수 있습니다.

*lpTimerFunc

WM_TIMER메세지가 발생할 때마다 호출할 콜백 함수를 지정하는 부분입니다. 그림을 보시면 자료형이 TIMERPROC라고 되어있는데 이 자료형은 WndProc와 같은 CALLBACK 함수임을 뜻합니다. 우리는 사용할 필요가 없으므로 NULL값을 줍시다.

자 이제 WM_PAINT메세지를 살펴봅시다.

- SetBkColor(hdc,RGB(0,100,200));
  SetTextColor(hdc,RGB(255,255,255));

출력할 글자의 배경색과 색깔을 지정해주기 위해 사용하였습니다.

- wsprintf(time_count,L"%d",time_value);
  TextOut(hdc,55,200,time_count,lstrlen(time_count));
  wsprintf(time_count,L"%d",landmine_value);
  TextOut(hdc,150,200,time_count,lstrlen(time_count));

wsprintf함수를 사용하여 배열에 데이터를 출력시킵니다. 첫번째 wsprintf함수는 time_count에 time_value값을 출력시켰습니다. 이 time_value값은 타이머의 값을 의미합니다. 그리고 TextOut함수로 hdc에 time_count배열에 들어있는 값을 출력시킵니다. 이렇게 되면 결과적으로 (%d,time_value) 값이 화면에 나타나게 됩니다.

두번 째 wsprintf함수도 마찬가지입니다. 이 함수는 남은 지뢰 개수를 배열에 저장시키고 TextOut함수로 화면에 출력시킵니다.

한가지 주의해야 하는 부분은 TextOut함수를 호출할 때 글자를 출력할 좌표를 지정하게 되는데 이 좌표는 RECT로 선언된 rt구조체의 영역 안에 포함되어 있어야 합니다. 그래야 나중에 무효화처리되어 WM_PAINT에서 다시 그릴때 정상적으로 그려지게 됩니다. 즉 time_value를 출력시킬 때의 좌표는 rt 영역 안이어야 하며 landmine_value를 출력시킬 때의 좌표는 rt2 영역 안이어야 합니다.

- timer = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(Timer));
  landmine_count = LoadBitmap(Global_hInstance,MAKEINTRESOURCE(LM_COUNT));

앞에서 선언한 HBITMAP형 timer와 landmine_count변수에 비트맵을 저장시키는 코드입니다.

- hdcsrc = CreateCompatibleDC(hdc);
   Bitmap = (HBITMAP)SelectObject(hdcsrc,timer);
   BitBlt(hdc,25,192,24,24,hdcsrc,NULL,NULL,SRCCOPY);
   DeleteObject(Bitmap);
   Bitmap = (HBITMAP)SelectObject(hdcsrc,landmine_count);
   BitBlt(hdc,175,192,24,24,hdcsrc,NULL,NULL,SRCCOPY);
   DeleteObject(Bitmap);
   DeleteDC(hdcsrc);

비트맵이 저장된 timer와 landmine_count를 BitBlt함수로 출력시키는 부분입니다.

이제 WM_TIMER메세지를 살펴봅시다.

- time_value++;
  InvalidateRect(hWnd,&rt,TRUE);

WM_TIMER메세지가 발생할 때마다 time_value값을 하나씩 증가시킵니다. 앞의 SetTimer함수에서 주기를 1000으로 주었으므로 1초마다 WM_TIMER메세지가 발생하고 이로 인해 time_value값이 하나 증가합니다. 그리고 값이 증가하였으니 InvalidateRect함수를 호출하여 무효화 영역을 그려 WM_PAINT메세지를 발생시키도록 하였습니다. 이 InvalidateRect함수에 쓰인 rt구조체는 타이머 값이 출력되는 영역을 의미하지요. 즉 타이머의 값을 다시 그려주기 위해 사용되었습니다.

마지막으로 WM_DESTORY메세지를 봅시다.

- KillTimer(hWnd,1);

SetTimer함수로 설정된 타이머는 따로 삭제를 해주지 않으면 윈도우 창이 종료된 후에도 계속 남아있어 메모리 누수가 발생하게 됩니다. 이를 방지하기 위해 KillTimer함수를 사용하여 만든 타이머를 지웁니다. 인자는 SetTimer를 호출헀을 때의 핸들과 이름입니다.

이제 결과물을 한 번 봅시다.

네 이것으로 지뢰찾기 다듬기 1을 마치겠습니다.

Posted by englishmath
,