자료 저장소

# 공통 대화상자

공통 대화상자 관련된 코드는 Comdlg32.dll이라는 DLL 파일에 의해 제공된다. 그래서 프로젝트에 공통 대화상자
코드를 작성하려면 Comdlg32.dll 파일을 링크해야 하는데 비주얼 C++로 만든 프로젝트는 디폴트로 라이브러리를
포함하고 잇으므로 별다른 지정없이도 공통 대화상자를 사용할 수 있다.
공통 대화상자에는 파일열기, 폰트 선택, 색상 선택, 인쇄, 찾기, 바꾸기 등등이 있으나 여기서는 간단하게 네 가지
종류의 공통 대화상자에 대해서만 실습해본다.


# 파일열기 대화상자

파일열기 대화상자가 제공해주는 것은 사용자가 선택한 파일의 경로일 뿐, 파일을 열거나 저장하는 것은 아니다.

BOOL GetOpenFileName(LPOPENFILENAME lpofn);
BOOL GetSaveFileName(LPOPENFILENAME lpofn);

전자는 읽을 파일명을 선택하는 것이고 후자는 저장할 파일명을 선택하는 것이다. 두 함수는 대화상자의 캡션 등
몇가지만 다르고 파일 이름을 입력받는 방법은 동일하다. 인수로 OPENFILENAME이라는 구조체 포인터 하나를
취하며 파일을 제대로 입력 받았으면 0이 아닌 값(TRUE)을 리턴하고 사용자가 입력을 취소했을 경우에 0(FALSE)
을 리턴한다. 이 함수 자체의 사용법은 지극히 간단하지만 제대로 사용하기 위해 정작 알아야할 OPENFILENAME
이라는 구조체는 만만치가 않다.

[OPENFILENAME 구조체]
typedefstruct tagOFNW { 
DWORD lStructSize; // OPENFILENAME 구조체의 크기를 지정, 버전 확인을 위해 사용
HWND hwndOwner; // 대화상자 소유자를 지정, 없을 경우 NULL
HINSTANCE hInstance; // 대화상자 템플릿을 사용할 경우 인스턴스 핸들 지정, NULL
LPCWSTR lpstrFilter; // 파일 형식 콤보 박스에 나타낼 필터
LPWSTR lpstrCustomFilter; // 사용자가 실행중에 선택한 커스텀필터를 저장하기 위한 버퍼
DWORD nMaxCustFilter; // 커스텀 필터 버퍼의 길이
DWORD nFilterIndex; // 파일 형식 콤보 박스에서 사용할 필터의 인덱스 지정
LPWSTR lpstrFile; // 파일 이름 에디트에 처음 나타낼 파일명 지정, 필요없을 경우 NULL
DWORD nMaxFile; // lpstrFile 멤버의 길이, 최소256문자 분의 길이를 가져야 함
LPWSTR lpstrFileTitle; // 파일의 이름을 돌려받기 위한 버퍼. 경로는 포함안됨. NULL
DWORD nMaxFileTitle; // lpstrFileTitle 멤버의 길이를 지정
LPCWSTR lpstrInitialDir; // 파일 찾기를 시작할 디렉토리를 지정
LPCWSTR lpstrTitle; // 대화상자의 캡션을 지정, 지정하지 않을 경우 디폴트 캡션 사용
DWORD Flags; // 대화상자의 모양과 동작을 지정하는 옵션을 성정하는 플래그
WORD nFileOffset; // lpstrFile 버퍼 내의 파일명 오프셋을 리턴한다.
WORD nFileExtension; // lpstrFile 버퍼 내의 파일 확장자 오프셋을 리턴한다.
LPCWSTR lpstrDefExt; // 사용자가 확장자를 입력 하지 않았을 경우 디폴트 확장자를 지정.
LPARAM lCustData; // 훅 프로시저로 보낼 사용자 정의 데이터
LPOFNHOOKPROC lpfnHook; // OFN_FNABLEHOOK 플래그가 지정되어 있을 때 훅 프로시저를 지정
LPCWSTR lpTemplateName; // OFN_ENABLETEMPLATE 플래그가 지정되어 있을때 템플릿을 지정
#if (_WIN32_WINNT >= 0x0500)
void * pvReserved; // 예약
DWORD dwReserved; // 예약
DWORD FlagsEx; // 대화상자 초기화에 사용할 확장 플래그를 지정.
#endif// (_WIN32_WINNT >= 0x0500)
}OPENFILENAMEW, *LPOPENFILENAMEW;

[왼쪽 마우스 클릭시 파일을 오픈 하는 case문]

case WM_LBUTTONDOWN: 
memset(&OFN,0,sizeof(OPENFILENAME));
OFN.lStructSize=sizeof(OPENFILENAME);
OFN.hwndOwner=hWnd;
OFN.lpstrFilter=TEXT("모든 파일(*.*)\0*.*\0");
OFN.lpstrFile=lpstrFile;
OFN.nMaxFile=MAX_PATH;

if(GetOpenFileName(&OFN)!=0)
{
wsprintf(str,TEXT("%s 파일을 선택했습니다."),OFN.lpstrFile);
MessageBox(hWnd,str,TEXT("파일 열기 성공"),MB_OK);
}
return0;


# 필터 지정

필터와 관련된 멤버는 총 4개가 있다. 이중 가장 중요한 멤버는 lpstrFilter이며 이 멤버에 필터를 구성하면
파일열기 대화상자의 파일 형식 콤보 박스에 필터를 보여준다. lpstrFilter에는 여러개의 필터를 줄수 있으며 각
필터는 설명과 패턴으로 구성되고 각 항목은 NULL로 구분된다. 마지막 항목은 두 개의 NULL로 끝나야 한다.

── 첫번째 필터 ─── 두번째 필터 ───
텍스트문서\0*.txt\0모든파일\0*.*\0
──설명───패턴───설명───패턴─

이 필터는 메모장에 파일열기 대화상자에 구성된 필터이다.
한 필터에 대해 두 개 이상의 패턴을 지정하려면"그래픽파일\0*.gif;*.jpg\0"처럼 세미콜론으로 패턴을 연결하면
된다. 사용자는 미리 제공되는 필터중 하나를 선택할 수도 있고 아니면 파일이름란에 *.mp3등과 같이 직접 필터를
만들어 사용할 수도 있다. lpstrCustomFilter는 사용자가 입력하는 필터를 조사하기 위한 멤버이며 이 버퍼에
커스텀 필터의 설명을 미리 작성해 놓으면 대화상자가 닫힐 때 패턴을 뒤에 붙여준다.


# 디렉토리 선택

파일 열기 대화상자의 실행 결과는 결국 사용자가 선택한 파일의 경로이다. 파일 열기 대화상자에 디렉토리도
표시되기는 하지만 디렉토리 자체를 선택할 수는 없다. 다운로드 파일을 저장할 폴더나 검색 시작 경로 등을 입력
받고 싶을 때 이 기능이 필요하다

[디렉토리 선택 예제]
#include <shlobj.h> 
BOOL BrowseFolder(HWND hParent, LPCTSTR szTitle, LPCTSTR StartPath, TCHAR *szFolder);

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
TCHAR *Mes=TEXT("왼쪽 마우스 버튼을 눌러 폴더를 선택하시오.");
static TCHAR StartPath[MAX_PATH];
TCHAR Folder[MAX_PATH];

switch (iMessage) {
case WM_CREATE:
hWndMain=hWnd;
return0;
case WM_LBUTTONDOWN:
if (BrowseFolder(hWnd,TEXT("폴더를 선택하시오"),StartPath,Folder)) {
MessageBox(hWnd,Folder,TEXT("선택한 폴더"),MB_OK);
lstrcpy(StartPath,Folder);
}
return0;
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
TextOut(hdc,10,10,Mes,lstrlen(Mes));
EndPaint(hWnd, &ps);
return0;
case WM_DESTROY:
PostQuitMessage(0);
return0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

int CALLBACK BrowseCallbackProc(HWND hwnd,UINT uMsg,LPARAM lParam,LPARAM lpData)
{
switch (uMsg) {
case BFFM_INITIALIZED:
if (lpData != NULL) {
SendMessage(hwnd,BFFM_SETSELECTION,TRUE,(LPARAM)lpData);
}
break;
}
return0;
}

BOOL BrowseFolder(HWND hParent, LPCTSTR szTitle, LPCTSTR StartPath, TCHAR *szFolder)
{
LPMALLOC pMalloc;
LPITEMIDLIST pidl;
BROWSEINFO bi;

bi.hwndOwner = hParent;
bi.pidlRoot = NULL;
bi.pszDisplayName = NULL;
bi.lpszTitle = szTitle ;
bi.ulFlags = 0;
bi.lpfn = BrowseCallbackProc;;
bi.lParam = (LPARAM)StartPath;

pidl = SHBrowseForFolder(&bi);

if (pidl == NULL) {
returnFALSE;
}
SHGetPathFromIDList(pidl, szFolder);

if (SHGetMalloc(&pMalloc) != NOERROR) {
returnFALSE;
}
pMalloc->Free(pidl);
pMalloc->Release();
returnTRUE;
}
이 대화상자는 공통 대화상자가 아니며 쉘이 제공하는 함수에 의해 지원된다.
이 대화상자를 출력하는 쉘 함수는 SHBrowseForFolder인데 이 함수는 COM 라이브러리로 작성되어 있어
메모리를 할당하는 방식도 특이하고 에러를 처리하는 방식도 다소 독특하다. 선택한 디렉토리를 문자열로
리턴하는 것이 아니라 ITEMIDLIST라는 생소한 타입으로 리턴하며 리턴된 ID로부터 실제 경로를 구하는 별도의
함수를 호출해야 한다. 게다가 이 함수 실행중에 별도의 메모리를 할당하여 리턴하므로 함수 리턴 후 동적 할당된
메모리까지 해제해야 한다. 위 예제는 자주 쓰는 기능만을 제공하는 다음 래퍼 함수를 작성해서 사용하고 있다.

BOOL BrowseFolder(HWND hParent, LPCTSTR szTitle, LPCTSTR StartPath, TCHAR *szFolder);

부모 윈도우와 타이틀 바의 캡션, 시작 디렉토리를 인수로 주고 마지막 인수에 선택한 폴더를 돌려 받기 위한
버퍼만 제공하면 된다. 리턴값을 사용자가 폴더를 선택했는지 아니면 취소했는지를 리턴한다. 이 함수와 바로위에
작성되어 있는 콜백함수만 복사해 가져가면 디렉토리 선택 대화상자를 언제든지 사용할 수 있다.


# 색상 대화상자

색상 대화상자는 사용자에게 색상을 입력받을 필요가 있을 때 사용한다. 색상값은 그 특성상 직접 눈으로 보지
않고 선택하기 어렵기 때문에 일반적인 방법으로 입력받기 곤란하다. 색상은 빨간색, 파란색, 초록색의 삼원색의
혼합으로 표현되는데 이 방식대로 색상을 선택하려면 3개의 정수값을 입력받아야 하며 게다가 수치만으로 실제
색상이 어떻게 나올지 바로 알 수 없으므로 무척 불편하다.
색상 선택 공통 대화상자를 호출하려면 다음 함수를 사용한다.

BOOL ChooseColor(LPCHOOSECOLOR lpcc);

파일 열기 대화상자와 마찬가지로 구조체 포인터만을 인수로 가진다. 사용자가 색상을 선택했으면 TRUE를 리턴,
취소했으면 FALSE를 리턴한다.

[CHOOSECOLOR 구조체]
typedefstruct tagCHOOSECOLORW { 
DWORD lStructSize;
HWND hwndOwner;
HWND hInstance;
COLORREF rgbResult; // 선택한 색상은 이 멤버로 전달된다.
COLORREF* lpCustColors; // 커스텀 색상을 전달하거나 돌려받기 위한 배열
DWORD Flags;
LPARAM lCustData;
LPCCHOOKPROC lpfnHook;
LPCWSTR lpTemplateName;
} CHOOSECOLORW, *LPCHOOSECOLORW;

[색상을 입력받아 사용하는 예제]
COLORREF Color=RGB(0,0,255); 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
HBRUSH MyBrush, OldBrush;
CHOOSECOLOR COL;
static COLORREF crTemp[16];
int i;

switch (iMessage) {
case WM_CREATE:
for (i=0;i<16;i++) {
crTemp[i]=RGB(255,255,255);
}
return0;
case WM_LBUTTONDOWN:
memset(&COL, 0, sizeof(CHOOSECOLOR));
COL.lStructSize = sizeof(CHOOSECOLOR);
COL.hwndOwner=hWnd;
COL.lpCustColors=crTemp;
if (ChooseColor(&COL)!=0) {
Color=COL.rgbResult;
InvalidateRect(hWnd, NULL, TRUE);
}
return0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
MyBrush=CreateSolidBrush(Color);
OldBrush=(HBRUSH)SelectObject(hdc,MyBrush);
Rectangle(hdc,10,10,300,200);
SelectObject(hdc,OldBrush);
DeleteObject(MyBrush);
EndPaint(hWnd,&ps);
return0;
case WM_DESTROY:
PostQuitMessage(0);
return0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

# 폰트 대화상자

글꼴도 무척 복잡한 정보이기 때문에 공통 대화상자를 사용하지 않으면 입력받기 무척 어렵다. 사용하는 함수는
ChooseFont 함수이다.

BOOL ChooseFont(LPCHOOSEFONT lpcf);

[CHOOSEFONT 구조체]

typedefstruct tagCHOOSEFONT{ 
DWORD lStructSize; // 구조체의 크기를 지정
HWND hwndOwner;
HDC hDC; // Flags에 CF_PRINTERFONTS가 설정되어있을 경우 프린터의 DC지정
LPLOGFONTW lpLogFont; // 사용자가 선택한 글꼴 정보를 리턴할 LOGFONT 구조체
INT iPointSize; // 10 * size in points of selected font
DWORD Flags; // enum. type flags
COLORREF rgbColors; // returned text color
LPARAM lCustData; // data passed to hook fn.
LPCFHOOKPROC lpfnHook; // ptr. to hook function
LPCWSTR lpTemplateName; // custom template name
HINSTANCE hInstance; // instance handle of.EXE that
// contains cust. dlg. template
LPWSTR lpszStyle; // return the style field here
// must be LF_FACESIZE or bigger
WORD nFontType; // same value reported to the EnumFonts
// call back with the extra FONTTYPE_
// bits added
WORD ___MISSING_ALIGNMENT__;
INT nSizeMin; // minimum pt size allowed &
INT nSizeMax; // max pt size allowed if
// CF_LIMITSIZE is used
} CHOOSEFONT;

[폰트대화상자를 이용한 폰트 변경 예제]
LOGFONT lf; 
COLORREF Col;
TCHAR *str=TEXT("폰트 대화상자 Test 1234");
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
CHOOSEFONT CFT;
HFONT MyFont, OldFont;
switch (iMessage) {
case WM_CREATE: // 기본 폰트 설정
lf.lfHeight=20;
lf.lfCharSet=HANGUL_CHARSET;
lf.lfPitchAndFamily=VARIABLE_PITCH | FF_ROMAN;
lstrcpy(lf.lfFaceName,TEXT("굴림"));
return0;
case WM_LBUTTONDOWN: // 폰트 대화상자 열기
memset(&CFT, 0, sizeof(CHOOSEFONT));
CFT.lStructSize = sizeof(CHOOSEFONT);
CFT.hwndOwner=hWnd;
CFT.lpLogFont=&lf; // lpLogFont를 통해 &lf에 지정한 폰트가 저장된다.
CFT.Flags=CF_EFFECTS | CF_SCREENFONTS; // 옵션 플래그 지정
if (ChooseFont(&CFT)) {
Col=CFT.rgbColors;
InvalidateRect(hWnd, NULL, TRUE);
}
return0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
MyFont=CreateFontIndirect(&lf);
OldFont=(HFONT)SelectObject(hdc,MyFont);
SetTextColor(hdc,Col);
TextOut(hdc,100,100,str,lstrlen(str));
SelectObject(hdc,OldFont);
DeleteObject(MyFont);
EndPaint(hWnd,&ps);
return0;
case WM_DESTROY:
PostQuitMessage(0);
return0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

# 찾기 대화상자

찾기 대화상자는 특정 문자열을 찾거나 다른 문자열로 바꿀수 있도록 검색 조건을 입력 받는 대화상자 일뿐
이 대화상자가 찾기, 바꾸기를 직접 하는 것은 아니다. 다른 대화상자와는 달리 검색 중에도 검색된 내용을 수정
할 수 있어야 하기 때문에 모델리스로만 동작한다.

HWND FindText(LPFINDREPLACE lpfr);
HWND ReplaceText(LPFINDREPLACE lpfr);

두 함수의 기능이 유사하기 때문에 사용하는 구조체도 동일 하다.

[FINDREPLACE 구조체]
typedefstruct tagFINDREPLACEW { 
DWORD lStructSize; // size of this struct 0x20
HWND hwndOwner; // handle to owner's window
HINSTANCE hInstance; // instance handle of.EXE that
// contains cust. dlg. template
DWORD Flags; // one or more of the FR_??
LPWSTR lpstrFindWhat; // ptr. to search string
LPWSTR lpstrReplaceWith; // ptr. to replace string
WORD wFindWhatLen; // size of find buffer
WORD wReplaceWithLen; // size of replace buffer
LPARAM lCustData; // data passed to hook fn.
LPFRHOOKPROC lpfnHook; // ptr. to hook fn. or NULL
LPCWSTR lpTemplateName; // custom template name
} FINDREPLACEW, *LPFINDREPLACEW;

[찾기 대화상자 예제]
#include <windows.h> 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;
LPCTSTR lpszClass=TEXT("FindDial");
UINT FRMsg;
HWND hDlgFR=NULL;
FINDREPLACE FR;
TCHAR szFindWhat[256];

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;
WndClass.lpszClassName=lpszClass;
WndClass.lpszMenuName=NULL;
WndClass.style=CS_HREDRAW | CS_VREDRAW;
RegisterClass(&WndClass);

hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
NULL,(HMENU)NULL,hInstance,NULL);
ShowWindow(hWnd,nCmdShow);

while (GetMessage(&Message,NULL,0,0)) {
if (!IsWindow(hDlgFR) || !IsDialogMessage(hDlgFR,&Message)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
}
return (int)Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
TCHAR *Mes=TEXT("찾기 대화상자를 보여줍니다");
FINDREPLACE *pFR;
staticint Count=1;
static TCHAR str[10000];
TCHAR sTmp[256];
RECT crt;

if (iMessage == FRMsg) {
pFR=(FINDREPLACE *)lParam;
// 대화상자 종료
if (pFR->Flags & FR_DIALOGTERM) {
hDlgFR=NULL;
return0;
}

// 다음 찾기
if (pFR->Flags & FR_FINDNEXT) {
wsprintf(sTmp,TEXT("%s 문자열을 %s 방향으로 대소문자 구분%s %s검색합니다\r\n"),
szFindWhat, (pFR->Flags & FR_DOWN ? TEXT("아래쪽"):TEXT("위쪽")),
(pFR->Flags & FR_MATCHCASE ? TEXT("하여"):TEXT("없이")),
(pFR->Flags & FR_WHOLEWORD ? TEXT("단어단위로 "):TEXT("")));
lstrcat(str,sTmp);
InvalidateRect(hWnd,NULL,TRUE);
}
return0;
}

switch (iMessage) {
case WM_CREATE:
// 메시지 등록
FRMsg=RegisterWindowMessage(FINDMSGSTRING);
return0;
case WM_LBUTTONDOWN:
// 대화상자 호출
if (hDlgFR == NULL) {
memset(&FR,0,sizeof(FINDREPLACE));
FR.lStructSize=sizeof(FINDREPLACE);
FR.hwndOwner=hWnd;
FR.lpstrFindWhat=szFindWhat;
FR.wFindWhatLen=256;

hDlgFR=FindText(&FR);
}
return0;
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
GetClientRect(hWnd,&crt);
crt.left=10;
crt.top=30;
TextOut(hdc,10,10,Mes,lstrlen(Mes));
DrawText(hdc,str,-1,&crt,0);
EndPaint(hWnd, &ps);
return0;
case WM_DESTROY:
PostQuitMessage(0);
return0;
}
return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
다른 공통 대화상자와는 달리 모델리스로 열리기 때문에 신경쓸 부분이 많다.
FINDREPLACE 구조체, 그리고 검색 조건과 바꿀 내용을 리턴받는 lpstrFindWhat,lpstrReplaceWith  버퍼는 반드시
전역 또는 스택이어야 한다. 이 변수들은 모델리스 대화상자가 떠 있는 동안 항상 참고 해야 하므로 지역변수로
선언하면 안된다.

모델리스 대화상자가 있는 프로젝트의 메시지루프에는 반드시 IsDialogMessage 함수 호출문이 포함되어야 하며
이를 생략하면 찾기/바꾸기 대화상자에서 키보드 처리가 제대로 되지 않는다.

FindText 함수는 대화상자를 띄우기만 할 뿐이므로 이 함수 호출 후 곧바로 리턴값을 받는 것이 아니고 실행중에
FINDMSGSTRING이라는 메시지로 리턴값을 받는다. 찾기/바꾸기 대화상자는 다음 찾기 또는 바꾸기등의 버튼을
누를 때 사용자가 입력한 옵션을 FINDREPLACE 구조체에 담아 이 메시지의 lParam으로 전달한다. 이메시지는
문자열로 등록된 메시지이므로 대화상자를 사용하는 프로그램은 반드시 이 메시지의 ID를 먼저 찾아놓아야한다.
예제에서는 WM_CREATE에서 FRMsg라는 변수에 이 메시지의 ID를 조사하였다.

메인 윈도우는 FINDMSGSTRING 메시지를 받았을 때 lParam으로 전달된 검색 옵션을 보고 적절한 검색을 해야
한다. 이 메시지의 ID는 시스템 상황에 따라 달라지기 때문에 실행중에 조사해 변수 FRMsg에 저장해 두었다.
그래서 case문 안에 이 메시지 처리문을 두지 못하고 반드시 if문으로 메시지를 검사해야 한다.
이 메시지로 FR_DIALOGTERM,FR_FINDNEXT,FR_REPLACE 등의 명령이 전달되는데 메인 윈도우는 이 플래그
로부터 어떤 동작을 할지 알아낸다.

찾기 대화상자는 어디까지나 찾을 내용과 옵션을 입력받아 메인 윈도우로 전달할뿐이지 결코 검색까지 하는것은
아니다. 응용 프로그램이 관리하는 데이터의 포맷은 대개의 경우 고유하기 때문에 공통 대화상자 따위가 검색을
할 수 없으며 검색 방법과 검색 후의 결과 출력, 검색 후의 처리등은 알아서 해야한다.

댓글 로드 중…

최근에 게시된 글