백택 (bektekk@yahoo.co.kr)
홈페이지 : http://bektekk.wo.to

문자열(스트링) 전격 분석 2부 2강(끝)

이제 실제 클래스들을 다룹니다. 사실 원문에는 VARIANT타입과
VC7 CLR 환경에서의 스트링에 관련된 부분까지 포함하고 있으나,
제가 CLR에 대해서는 문외한인 관계로 생략했고,
VARIANT부분은 특별히 의미가 있어 보이지 않아 생략했음을 알려드립니다.

본강좌는 코드 프로젝트에 제가 좋아하는 프로그래머인 Michael Dunn의 강좌
The Complete Guide to C++ Strings, Part I 과 II 를 번역한 글입니다.
최대한 의역을 하려고 노력했지만, 이런쪽의 경험이 부족하다 보니 많이 모자란
강좌가 되겠지만, 많은 도움이 되셨으면 합니다.
기타 문의 사항이나 질문은 쪽지나 메일을 이용해 주셨으면 합니다.
본강좌는 제 홈페이지(위의 링크)를 통해서도 보실수 있습니다.

 

스트링 랩퍼 클래스들

지금까지 여러가지의 스트링 타입에 대해 설명했고, 이제 스트링 랩퍼 클래스들에 대해 설명하겠다. 각각 어떻게 생성을 하고 어떻게 C스타일 스트링으로 변환시킬수 있는지 설명하겠다. 왜냐하면 C스타일 스트링 포인터는 API함수를 호출하거나 다른 랩퍼클래스들을 생성하는데 필요하기 때문이다. 나는 그외의 함수들 예를 들자면 정렬, 비교 등의 기능들에 대해서는 생략하도록 하겠다.

다시한번 강조하지만, 확실하지 않다면 절대로 강제 형변환을 하지 않도록 하자.

CRT에서 제공하는 클래스들

_bstr_t

_bstr_t 클래스는 아주 잘 만들진 BSTR 랩퍼 클래스다. 그리고 BSTR의 내부구현이나 기타 등등의 문제를 랩핑 했다. 이 클래스는 여러가지 연산자를 재정의 했을 뿐만 아니라, 사용자의 편의를 위해 다양한 생성자를 제공한다. 하지만 BSTR자체에 접근하는 연산자들은 없다는 걸 주의해라. 따라서 [out] 파라미터를 갖는 COM 메서드에 직접적으로 전달할수는 없다. 만약 COM메서드에 직접적으로 전달할수 있는 BSTR* 타입이 필요하면 ATL 클래스인 CComBSTR 이 더 좋은 선택일 것이다.

하지만 물론 _bstr_t 도 BSTR을 받는 함수에 전달할 수 있다. 이는 다음의 세가지 이유 때문이다. 첫째 _bstr_t  wchar_t* 으로의 형변환 연산자를 제공한다. 둘째, wchar_t*  BSTR 는 컴파일 과정에서는 같은 타입으로 인식된다. 왜냐하면 BSTR은 wchar_t*형으로 정의되 있기 때문이다. 마지막 세번째는 _bstr_t 는 내부적으로 문자열의 시작부분을 포인터로 가리키고 있기 때문이다. 따라서 명시적으로 형변환 연산자가 없어도 잘 작동되곤 한다.

// 생성
_bstr_t bs1 = "char string";       // LPCSTR로 부터 생성
_bstr_t bs2 = L"wide char string"; // LPCWSTR로 부터 생성
_bstr_t bs3 = bs1;                 // 다른 _bstr_t로 부터 생성
_variant_t v = "Bob";
_bstr_t bs4 = v;                   // 스트링을 포함하는 _variant_t로 부터 생성
 
// Extracting data
LPCSTR psz1 = bs1;              // 자동으로 MBCS로 형변환
LPCSTR psz2 = (LPCSTR) bs1;     // 위의 경우와 같다.
LPCWSTR pwsz1 = bs1;            // 내부적으로 포함하는 유니코드 스트링 리턴
LPCWSTR pwsz2 = (LPCWSTR) bs1;  // 위의 경우와 같다.
BSTR    bstr = bs1.copy();      // 복사
 
  // ...
  SysFreeString ( bstr ); //메모리에서 해제해 주어야 한다.

_bstr_t 은 또한 char*  wchar_t* 에 대한 형변환 연산자도 가지고 있다. 하지만 이는 의문스러운 구현이다. 이 연산자들에 의해 리턴되는 포인터는 const 타입이 아니여서 수정될수도 있지만, 만약 수정된다면 내부적인 BSTR 타입이 깨질것이고, 많은 문제를 유발할 것이다.

STL 클래스들

STL은 basic_string 하나의 스트링 클래스가 있다. basic_string 은 0으로 끝나는 문자배열 스트링을 관리한다. 어떤 캐릭터 타입이 쓰일지에 대한것은 템플릿 매개변수로 전달이 된다. 일반적으로 이 클래스에서 얻은 포인터를 직접적으로 조작하는 일은 피해야 한다. 대신 이 클래스에서 제공하는 함수나 연산자들을 써서 조작하기를 권장한다.

이 클래스에서 미리정해진 두가지의 형태의 타입이 존재한다. 하나는 char 를 조작하는 string이고 다른 하나는 wchar_t를 조작하는 wstring 이다. 사실, TCHAR 형의 스트링 클래스는 정의되 있지 않지만 아래의 코드를 통해 TCHAR타입도 사용할 수가 있다.

// 타입 정의
typedef basic_string<TCHAR> tstring; // TCHAR형 스트링
 
// 생성자들
string str = "char string";         // LPCSTR로 부터 생성
wstring wstr = L"wide char string"; // LPCWSTR로 부터 생성
tstring tstr = _T("TCHAR string");  // LPCTSTR로 부터 생성
 
// Extracting data
LPCSTR psz = str.c_str();    // 읽기 전용 즉 const형 버퍼 리턴
LPCWSTR pwsz = wstr.c_str(); // 읽기 전용 즉 const형 버퍼 리턴 LPCWSTR형
LPCTSTR ptsz = tstr.c_str(); // 읽기 전용 즉 const형 버퍼 리턴 LPCTSTR형

_bstr_t형과는 다르게 basic_string 클래스는 다른 캐릭터 타입들끼리 직접적으로 형변환을 할수 없다. 그러나 c_str() 함수에서 전달되는 포인터를 이용해서 다른 클래스를 생성할 수 있다. 하지만 그 다른 클래스에는 그 포인터를 받는 생성자가 존재해야만 할 것이다. 예를 들면 :

// 예제 basic_string타입을 이용해서 _bstr_t 형 객체 생성
_bstr_t bs1 = str.c_str();  // LPCSTR 로 부터 생성
_bstr_t bs2 = wstr.c_str(); // LPCWSTR 로 부터 생성

ATL 클래스들

CComBSTR

CComBSTR 은 ATL의 BSTR랩퍼 클래스다. 몇몇 곳에서 _bstr_t 보다 더 유용하게 쓰인다. 가장 중요한 부분은 CComBSTR 은 내부에 존재하는 BSTR 을 직접적으로 접근할수 있다는 데 있다. 이 얘기는 즉, COM 메서드에 직접적으로 CComBSTR 객체를 전달할수 있다는 얘기가 된다. 그럼 CComBSTR 객체는 자동으로 BSTR 에 관련된 메모리를 관리해 줄것이다. 예를 들어 아래의 인터페이스의 메서드를 호출한다고 가정해보자. :

// 간단한 인터페이스
struct IStuff : public IUnknown
{
  // 생략....

  STDMETHOD(SetText)(BSTR bsText);
  STDMETHOD(GetText)(BSTR* pbsText);
};

CComBSTR  BSTR 형변환 연산자를 가지고 있다. 따라서 객체를 직접적으로 SetText() 메서드에 전달할 수가 있다. 또한 & 연산자를 제정의 해서 BSTR* 타입을 리턴한다. 그래서 여러분은 BSTR* 타입을 받는 메서드에 부가적인 형변환이 없이도 직접적으로 전달 할수 있게 된다.

CComBSTR bs1;
CComBSTR bs2 = "new text";
 
  pStuff->GetText ( &bs1 );       // 내부 BSTR 의 포인터 전달
  pStuff->SetText ( bs2 );        // BSTR 로 암시적 형변환
  pStuff->SetText ( (BSTR) bs2 ); // 명시적 형변환 위의 경우와 정확히 같다.

CComBSTR  _bstr_t 타입과 거의 유사한 생성자 구조를 가지고 있다. 하지만 중요한 점은 MBCS 스트링을 자동적으로 변환해 주는 생성자는 없다는 것이다. 따라서 MBCS 스트링을 사용할 경우에는 여러가지 방법으로 BSTR형으로 변환해 주어야만 한다. 그 중에서 ATL 변환 마크로 들을 추천한다.

// 생성
CComBSTR bs1 = "char string";       // LPCSTR 로 부터 생성
CComBSTR bs2 = L"wide char string"; // LPCWSTR 로 부터 생성
CComBSTR bs3 = bs1;                 // CComBSTR  로 부터 복제생성
CComBSTR bs4;
 
  bs4.LoadString ( IDS_SOME_STR );  // 스트링 테이블 리소스로 부터 생성
 
// 값 얻어오기
BSTR bstr1 = bs1;        // 내부의 BSTR을 리턴 하지만 조작하지는 말것!!
BSTR bstr2 = (BSTR) bs1; // 형변환도 OK
BSTR bstr3 = bs1.Copy(); // 같은 내용의 BSTR을 생성후 리턴
BSTR bstr4;
 
  bstr4 = bs1.Detach();  // bs1 내부의 BSTR을 더이상 관리하지 않는다.
 
  // ...
  SysFreeString ( bstr3 );
  SysFreeString ( bstr4 );

위의 예제에서 Detach() 메서드 부분을 주의해서 보자. 이 메서드의 호출후에는 CComBSTR 객체는 더이상 BSTR을 관리하지 않는다. 따라서 마지막부분의SysFreeString() 함수를 호출해 줄 필요가 있는 것이다.

마지막으로 중요한 문제는 위 클래스가 & 연산자를 정의했다는 부분이다. 즉, 이 얘기는 STL 컬렉션 클래스 들에는 CComBSTR타입을 직접적으로 쓸수 없다는 얘기이다. 왜냐하면 STL컬렉션들은 & 연산자는 내부적으로 관리하는 객체 리스트 들의 주소값을 리턴해야만 하는데 CComBSTR 클래스에서 & 연산자를 정의함으로써 CComBSTR* 을 리턴하는게 아니라, BSTR*를 리턴 하게 된다. 따라서 이 경우에는 아래의 예처럼 CAdapt 클래스로 처리해 주어야 한다. :

  std::list< CAdapt<CComBSTR> > bstr_list;

CAdapt 클래스는 컬랙션 클래스에 필요한 연산자들을 다시 정의 하고 있다. 따라서 CComBSTR 의 리스트를 사용할수 있게 된다.

ATL 형변환 매크로들

ATL의 스트링 형변환 매크로 들은 스트링 사이의 인코딩을 변경하는 매우 편리한 방법을 제공한다. 특히, 함수호출 시에 매우 편리하다. 매크로의 이름들도 명료하다. [소스 타입]2[새로운 타입] 혹은 [소스 타입]2C[새로운 타입]. 두번째의 매크로는 C라는 글자를 포함하는데 이는 const형(읽기전용)으로의 변환을 의미한다. 각각의 타입에 대한 정의는 다음과 같다.:

A: MBCS 스트링, char* (A 는 ANSI를 의미)
W: Unicode 스트링, wchar_t* (W는 wide를 의미)
T: TCHAR 스트링, TCHAR*
OLE: OLECHAR 스트링, OLECHAR* (사실, W와 같은 의미)
BSTR: BSTR

그래서, 예를 들면, W2A() 매크로는 유니코드 스트링을 MBCS 스트링으로 변환해 준다. 그리고 T2CW() 매크로는 TCHAR 스트링을 읽기전용 유니코드 스트링으로 변환해 준다.

이 매크로들을 사용하기 위해서는 먼저 atlconv.h 헤더를 포함해 준다.(위의 매크로들은 ATL프로젝트가 아니여도 사용할수 있다.!! 왜냐하면 위의 헤더 파일은 다른 ATL 부분과의 의존성이 없다. 즉 따로 놀기 때문이다. 그리고 ATL의 _Module 객체를 필요로 하지 않는다.)  둘째로, 해더를 포함했으면 위의 매크로들을 사용하기에 앞서 USES_CONVERSION 라는 매크로를 써주어야 한다. 위의 매크로는 형변환을 하기위해 필요한 몇몇 지역변수들을 선언한다. 즉, 형변환을 하기위한 지저분한 일들을 해준다.

변환하고자 할 타입이 BSTR 타입이 아닐때는 변환된 타입은 스택에 저장된다. 따라서 함수범위를 넘어서 작업을 하고 싶으면 여러분은 다른 스트링 클래스에 복사를 해서 저장 하든지 해서 계속적으로 사용할수 있을 것이다. 하지만 BSTR타입의 경우는 ::SysFreeString등의 함수로 해제를 시켜 주어야 한다.

아래는 변환 매크로 들을 사용하는 예제 코드 이다. :

// 다양한 형태를 받는 함수들:
void Foo ( LPCWSTR wstr );
void Bar ( BSTR bstr );
// 스트링을 리턴하는 함수(BSTR*형에 주의):
void Baz ( BSTR* pbstr );
 
#include <atlconv.h>
 
main()
{
  using std::string;
  USES_CONVERSION;    // 형변환 매크로를 사용하기위해서
 
  // 예제 1 : Foo()함수에 MBCS를 전달
  LPCSTR psz1 = "Bob";
  string str1 = "Bob";
 
  Foo ( A2CW(psz1) );
  Foo ( A2CW(str1.c_str()) );
 
  // 예제 2: Bar()함수에 MBCS 와 유니코드 스트링 전달
  LPCSTR psz2 = "Bob";
  LPCWSTR wsz = L"Bob";
  BSTR bs1;
  CComBSTR bs2;
 
  bs1 = A2BSTR(psz2);         // BSTR 생성
  bs2.Attach ( W2BSTR(wsz) ); // CComBstr에 할당
 
  Bar ( bs1 );
  Bar ( bs2 );
 
  SysFreeString ( bs1 );      // 반드시 해제해 주어야 한다.
  // bs2의 경우는 CComBstr의 소멸자가 알아서 해재해 준다.
 
  // 예제 3: Baz()함수에서 리턴반든 BSTR형의 형변환
  BSTR bs3 = NULL;
  string str2;
 
  Baz ( &bs3 );          // Baz()를 통해 BSTR을 얻었다.
 
  str2 = W2CA(bs3);      // MBCS 스트링으로 형변환
  SysFreeString ( bs3 ); // 메모리 해제
}

보시다 시피, 위의 매크로 들은 매우 간편하다. 이제 몇가지 사실들에만 주의하면 어디서든 편하게 위의 매크로들을 사용할수 있을 것이다.

MFC 클래스들

CString

MFC의 CString 클래스는 TCHAR 형 타입을 다룬다. 따라서 정확한 캐릭터 타입은 컴파일 시간에 전처리명령어를 통해 달라진다. 일반적으로 CString 타입은 STL의 string 타입과 유사하다. 왜냐하면 여러분은 CString을 다룰때 std::string 에서 처럼 직접적으로 문자열 조작을 피하고 제공되는 함수들을 써야 하기 때문이다. 한가지 STL의 스트링 클래스보다 CString클래스가 편리한 점은 CString클래스는 MBCS 스트링과 유니코드 스트링을 모두 수용하는 생성자를 가지고 있다는 사실이다. 그리고 LPCTSTR형변환 연산자를 제공해서 여러분은 LPCTSTR을 받는 함수에 특별한 형변환 없이 전달 할수 있다. std::string에서 처럼 c_str()들의 메서드를 호출할 필요가 없다.

  // 생성자
  CString s1 = "char string";  // LPCSTR 로 부터 생성
  CString s2 = L"wide char string";  // LPCWSTR 로 부터 생성
  CString s3 ( ' ', 100 );  // 100바이트를 할당하고 스페이스 문자로 채운다. 
  CString s4 = "New window text";
 
  // LPCTSTR을 받는 함수에 직접 전달할수 있다.
  SetWindowText ( hwndSomeWindow, s4 );
 
  // 혹은 명시적 형변환으로 위의 경우와 정확히 같다.
  SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );

또한 리소스의 스트링 테이블을 좀 더 편하게 읽어올수 있다. 생성자에서도 그런일을 해주고 Format() 메서드에서도 그런 일이 가능하다.

  // 스트링 테이블의 문자열로 부터 생성
  CString s5 ( (LPCTSTR) IDS_SOME_STR );  
  CString s6, s7;
 
  // LoadString메서드
  s6.LoadString ( IDS_SOME_STR );
 
  // printf스타일로 포맷팅
  s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );

첫번째의 생성자는 약간 이상해 보인다. 하지만 이는 정확히 문서화 되 있는 문자열 테이블로 부터 문자열을 생성하는 방법이다.

CString에 적용될수 있는 형변환은 LPCTSTR 타입 뿐이다. LPTSTR (즉 non-const) 로의 형변환은 잘못된 것이다. 많은 사람들이 LPTSTR형으로 변환하고 사용하는 일을 종종 보게 되는데 이는 여러가지 문제를 야기할 것이다. 나중에 버그를 찾기도 굉장히 어렵게 만든다. ^^ 따라서 LPTSTR형의 값을 얻으려면GetBuffer() 메서드를 써야만 한다.

리스트 콘트롤에 텍스트를 지정한다고 가정하고 다음 예제를 보자.:

  CString str = _T("new text");
  LVITEM item = {0};
 
  item.mask = LVIF_TEXT;
  item.iItem = 1;
  item.pszText = (LPTSTR)(LPCTSTR) str; // 잘못된 사용!
  item.pszText = str.GetBuffer(0);      // 올바른 사용
 
  ListView_SetItem ( &item );
  str.ReleaseBuffer();  // 위의 버퍼의 관리를 다시 CString에서 하게끔 해제

위의 예에서 pszText 멤버는 LPTSTR 타입이다. 그러므로 GetBuffer() 메서드를 사용해 주어야 한다. GetBuffer() 의 매개변수는 여러분이 할당하기를 원하는 버퍼의 최소 길이를 지정해 준다. 만약 1Kb의 버퍼를 원한다면 GetBuffer(1024) 이런식으로 호출해 주면 될것이다. 0을 전달하는 것은 현재 스트링이 포함한 스트링의 길이와 정확히 같은 길이의 버퍼를 리턴한다.

위의 사선으로 표시한 잘못된 예제는 컴파일 되고 사실 잘 작동되는것 처럼 보인다. 그렇지만 그 코드는 분명 잘못된 코드이다. 일단 위의 코드는 객체지향의 개념에 어긋나는 것이다. 캡슐화 되어있는 데이터를 직접조작하는 습관은 좋지 못하다. 그리고 내부적으로 관리되는 데이터의 구조를 무너뜨릴 가능성이 크다. 따라서 나중에 분명 원하지 않는 결과를 초래 할 것이다.

요즘의 많은 소프트웨어들은 모두 버그를 포함하고 있다. 이는 프로그래머들의 잘못된 습관에서 나오는 경향이 많다. 따라서 항상 올바른 프로그래밍 습관을 만들려고 노력하자. 버그가 0%가 되는 그날까지...

CString 클래스는 또한 BSTR을 만들어 주는 두가지 함수가 있다. AllocSysString()  SetSysString(). SetSysString() 이 BSTR* 인자를 받는다는 점만 빼고 , 위 두함수는 정확히 같은 일은 한다.

  // BSTR로 변환
  CString s5 = "Bob!";
  BSTR bs1 = NULL, bs2 = NULL;
 
  bs1 = s5.AllocSysString();
  s5.SetSysString ( &bs2 );
 
  // ...
  SysFreeString ( bs1 );
  SysFreeString ( bs2 );

WTL 클래스들

CString

WTL의 CString 클래스는 MFC의 CString 클래스와 정확히 똑같이 작동한다. 따라서 MFC의 CString부분을 참고하길 바란다.

printf스타일의 포맷팅 함수에서의 스트링 클래스 사용

여러분은 printf() 혹은 비슷한 방식으로 작동하는 함수들을 사용할때 특별히 주의해야 한다. 이런 함수들에는 sprintf() 등이 있고, 또한 TRACE  ATLTRACE 매크로도 똑같은 방식으로 작동한다. 이러한 함수나 매크로에서는 특별히 타입체킹을 하지 않기 때문에 여러분은 반드시 스트링 객체를 직접 전달하지 않고 C스타일 스트링을 전달 해야만 한다.

예를 들면, _bstr_t 객체가 가지고 있는 스트링을 ATLTRACE() 에 전달할때 여러분은 반드시 명시적으로 (LPCSTR)  (LPCWSTR) 로 형변환을 해 주어야 한다.:

  _bstr_t bs = L"Bob!";
  ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);

만약 형변환을 깜박했다면 아마 예상치 못한 결과가 출력될것이다.

총 정 리

스트링 클래스들 사이의 형변환을 일반적으로 다음의 방식을 따른다. 일반 소스 스트링을 C스타일 스트링으로 변환한 후 그것을 이용해 새로운 스트링 타입으로 생성하는 것이다. 따라서 아래의 표는 각각의 클래스 들이 C스타일로 형변환 할수 있는지 그리고 C스타일 스트링으로부터 객체를 생성할수 있는지 요약해서 보여주고 있다.

Class string
type
convert 
to char*?
convert to
const char*?
convert to
wchar_t*?
convert to
const wchar_t*?
convert
to BSTR?
construct
from char*?
construct
from wchar_t*?
_bstr_t BSTR yes, cast1 yes, cast yes, cast1 yes, cast yes2 yes yes
_variant_t BSTR no no no cast to
_bstr_t3
cast to
_bstr_t3
yes yes
string MBCS no yes, c_str()
method
no no no yes no
wstring Unicode no no no yes, c_str()
method
no no yes
CComBSTR BSTR no no no yes, cast
to BSTR
yes, cast yes yes
CComVariant BSTR no no no yes4 yes4 yes yes
CString TCHAR no6 in MBCS
builds, cast
no6 in Unicode
builds, cast
no5 yes yes
COleVariant BSTR no no no yes4 yes4 in MBCS builds in Unicode builds
1 Even though _bstr_t provides conversion operators to non-const pointers, modifying the underlying buffer may cause a GPF if you overrun the buffer, or a leak when the BSTR memory is freed.
2 A _bstr_t holds a BSTR internally in a wchar_t* variable, so you can use the const wchar_t* converter to retrieve the BSTR. This is an implementation detail, so use this with caution, as it may change in the future.
3 This will throw an exception if the data cannot be converted to a BSTR.
4 Use ChangeType() then access the bstrVal member of the VARIANT. In MFC, this will throw an exception if the data cannot be converted.
5 There is no BSTR conversion function, however the AllocSysString() method returns a new BSTR.
6 You can temporarily get a non-const TCHAR pointer using the GetBuffer() method.
Posted by 나비:D
:

백택 (bektekk@yahoo.co.kr)
홈페이지 : http://bektekk.wo.to

문자열(스트링) 전격 분석 2부 1강

본강좌는 코드 프로젝트에 제가 좋아하는 프로그래머인 Michael Dunn의 강좌
The Complete Guide to C++ Strings, Part I 과 II 를 번역한 글입니다.
최대한 의역을 하려고 노력했지만, 이런쪽의 경험이 부족하다 보니 많이 모자란
강좌가 되겠지만, 많은 도움이 되셨으면 합니다.
기타 문의 사항이나 질문은 쪽지나 메일을 이용해 주셨으면 합니다.
본강좌는 제 홈페이지(위의 링크)를 통해서도 보실수 있습니다.

이번강 까지도 좀 지루하겠네요.
다음강을 마지막으로 실제 클래스들을 다루어 보겠습니다.

소개

C스타일 스트링은 에러를 유발하기 쉽고, 관리하기가 매우 까다로울뿐만 아니라, 해커에게 오버런 버그를 노출하는 타겟이 될 수도 있기 때문에, 수많은 스트링 랩퍼 클래스들이 생겨났다. 하지만 불행히도 어떤 상황에서 어떤 클래스를 써야하고 혹은 C스트링을 어떻게 랩퍼 클래스를 이용해 처리해야 하는지는 가끔씩 우리를 해깔리게 한다.

이번 강좌 2부의 내용은 Win32 API, MFC, STL, WTL과 VC런타임 라이브러리의 모든 스트링 랩퍼 클래스들을 다루고 있다. 각 클래스는 어떻게 생성하고 사용하며, 각각 어떻게 변환할수 있는지 설명할 것이다.

2부의 내용을 제대로 소화하기 위해서는 1부 1,2강에서 설명한 캐릭터 타입들과 인코딩 방식에 대한 충분한 이해가 필요할 것이다.

스트링 클래스의 가장 중요한 규칙

만약 문서화되어 있어 형변환에 문제가 없다는 확신이 없으면 강제 형변환은 피할것.

어떤 스트링랩퍼 클래스 X와 또다른 클래스 Z사이의 형변환에 관한 내용은 특히 초급 프로그래머들이 많이 궁금해 하는 내용이다. 주로 초급 프로그래머들은 강제 형변환을 시도한 후 왜 제대로 동작하지 않는가에 대해 많이 궁금해 하곤 한다. 수많은 스트링 타입들, 특히 BSTR 같은 타입은 명확히 문서화 되어있지 않은 게 현실이다. 따라서 많은 사람들은 쉽게 잘못된 코드를 작성할 가능성이 많았다.

무엇보다 중요한 점은 랩퍼클래스에서 형변환 연산자를 재정의 하지 않았다면 형변환은 사실 스트링 사이에서 아무 일도 하지 않는다. 그래서 만약 아래와 같은 코드를 작성 했다면:

void SomeFunc ( LPCWSTR widestr );
 
main()
{
  SomeFunc ( (LPCWSTR) "C:\\foo.txt" );  // WRONG!
}

위의 코드는 백이면 백 잘못된 결과를 초래할 것이다. 사실 위의 코드는 에러 없이 컴파일 된다. 하지만 컴파일이 된다고 그 코드가 옳바르다고 장담할수는 없는 것이다.

강의를 진행하면서 어떤 형변환이 옳바른 것인지 설명하도록 하겠다.

C-style strings and typedefs

지난 강의에서 살펴 보았듯이, Win API는 TCHAR 방식으로 정의되 있다. 그것은 컴파일 시에 문맥에 따라 MBCS방식으로 전환될수도 있고, 유니코드가 될수도 있다. 편의를 위해 지난 강의에 보여줬던 표를 다시 보여 주도록 하겠다.

타입 MBCS환경에서 유니코드 환경에서
WCHAR wchar_t wchar_t
LPSTR 0으로 끝나는 char형의 문자열(char*) 0으로 끝나는 char형의 문자열 (char*)
LPCSTR 0으로 끝나는 const char형의 문자열 (const char*) 0으로 끝나는 const char형의 문자열 (const char*)
LPWSTR 0으로 끝나는 유니코드형의 문자열 (wchar_t*) 0으로 끝나는 유니코드형의 문자열 (wchar_t*)
LPCWSTR 0으로 끝나는 const 유니코드형의 문자열 (const wchar_t*) 0으로 끝나는 const 유니코드형의 문자열 (const wchar_t*)
TCHAR char wchar_t
LPTSTR 0으로 끝나는 TCHAR형의 문자열 (TCHAR* -> char*) 0으로 끝나는 TCHAR형의 문자열 (TCHAR*->wchar_t*)
LPCTSTR 0으로 끝나는 const TCHAR형의 문자열 (const TCHAR*) 0으로 끝나는 const TCHAR형의 문자열 (const TCHAR*)

타입을 하나 추가 하자면 OLECHAR을 들수 있겠다. 이 타입은 주로 자동화 인터페이스에서 사용된다. 이 타입은 보통 wchar_t 으로 정의되 있으나, 셋팅을 변경하거나 #define 문으로 OLE2ANSI 를 정의하면 단순 char 타입으로 전환된다. 하지만 사실상 요즘엔 OLE2ANSI 를 적용할 이유는 없다. (사실, MFC3 버젼에서 사용되어졌던 것이다. 구시대의 유물^^), 따라서 지금부터 그냥 단순히 OLECHAR 타입을 유니코드로 간주할 것이다.

아래의 표는 OLECHAR 와 관계된 typedef 문으로 정의된 데이터 타입을 보여준다. :

타입 의미
OLECHAR 유니코드 케릭터 (wchar_t)
LPOLESTR 유니코드 스트링 (OLECHAR*)
LPCOLESTR const 형 유니코드 스트링 (const OLECHAR*)

문자열을 다룰 때 유니코드 MBCS 방식에 관계없이 일관된 표현을 할수 있게 해주는 두가지 마크로가 있다. _T 마크로는 지난 강의에서도 다루었던 부분이다. :

마크로 의미
_T(x) L 유니코드빌드일때 L을 앞에 주가해 준다.
OLESTR(x) LPOLESTR 타입으로 만들기 위해 L을 앞에 추가해 준다.

또한 _T에서 변형된 형태의 하지만 같은 역활을 하는 몇몇 마크로 들이 더 있다. -- TEXT, _TEXT, __TEXT, and __T 이 마크로 들은 모두 같은 일을 한다.

COM에서의 스트링 - BSTR

많은 자동화객체 인터페이스나 COM 인터페이스는 스트링으로 BSTR 타입을 사용한다. 하지만 BSTR타입은 다루기 매우 까다롭고, 에러를 유발하기 쉽다. 따라서 BSTR을 이번 파트에 다뤄 보도록 하겠다.

BSTR 타입은 파스칼 스타일의 스트링과 C 스트링 사이의 잡종 즉 서로 짬뽕되서 생긴 타입이다. 파스칼에서는 문자열타입에 그 길이가 내부적으로 저장된다. 하지만 C스트링에서는 마지막 제로바이트를 통해서 그 문자열의 끝을 알수있게끔 되어 있다. 사실 BSTR 타입은 문자일 길이를 문자열 시작 바로 전에 저장하고 이어서 유니코드 스트링을 저장하는 방식의 타입니다. 또한 제로바이트로 그 끝을 표시한다. 아래는 "Bob" 이라는 BSTR 타입의 스트링의 메모리 구조를 보여준다.:

06 00 00 00  42 00  6F 00  62 00  00 00

길이

B

o

b

스트링끝

눈치채셨다 시피 문자열의 길이가 DWORD타입으로 (즉 4바이트) 실제 문자 앞에 저장된다. 하지만 이는 마지막 00 00의 제로바이트를 포함하지 않은 길이 이다. 위의 경우 "Bob" 문자열은 총 6바이트의 3개의 유니코드 캐릭터를 가지고 있다. 길이 정보를 포함하는 이유는 COM 라이브러리가 다른 곳으로 마샬링 될때 얼마나 많은 바이트를 보내야 하는지 그 정보가 필요하기 때문이다. (사실, BSTR 은 단지 스트링뿐만 아니라 임의의 어떤 데이타가 들어 있어도 상관이 없다.)

BSTR 변수는 C++에서 첫번째 캐릭터를 가리키는 포인터 변수 이다. BSTR 타입은 이렇게 정의 되있다. :

  typedef OLECHAR* BSTR;

불행히도 이런방식의 정의는 많은 문제를 유발할수 있다. 위에서도 설명했듯이 실제로는 BSTR은 유니코드 스트링과는 다른 특성을 가진 타입이다. 따라서 BSTR과 LPOLESTR은 마음대로 섞어 써도 컴파일러는 에러를 발생하지 않는다. LPOLESTR을 인자로 받는 함수에 BSTR 타입을 전달하는 것은 안전하다. 그렇지만 그 반대의 경우는 다르다. 따라서 함수가 받는 인자의 타입을 정확히 알고 정확히 전달하는 것이 중요하다.

BSTR 타입을 받는 함수에 LPOLESTR을 전달하는것이 왜 안전하지 못하냐 하면, BSTR이 가리키는 메모리 바로 앞 4바이트는 그 문자열의 길이를 포함하는 정보를 담고 있어여 한다. 아마 BSTR을 받는 함수에서는 그 정보가 포함되 있다는 가정하에 그 정보를 이용할 것이다. 하지만 LPOLESTR 타입에는 그러한 정보는 없다. 이는 안전하지 못한 결과를 낳을 것이다. 위에서 말했다 시피 COM객체에서는 BSTR 의 문자열 앞에 존재하는 문자열길이 정보를 이용해 그 만큼의 데이터를 전송한다고 있다. BSTR대신 LPOLESTR을 전달하면 얼마만큼의 바이트가 전송될지는 아무도 장담할 수가 없게 되는 것이다.

따라서 BSTR을 다루기 위한 몇몇 API들이 존재하지만 그중 가장 중요한 것은 두가지 이다. 하나는 BSTR을 생성시키기 위한 것이고, 다른하나는 제거하기 위한 것이다. BSTR을 생성하는 함수는 SysAllocString() 이고 제거하는것은 SysFreeString()이다.  SysAllocString() 은 매개변수로 받은 유니코드 스트링을 BSTR형태로 만들어 주는 역할은 한다. (새로운 메모리를 할당한 후에 아마 문자열 길이를 계산해서 문자열 앞에 그 길이정보를 추가해 주는 정도의 일을 할 것이다.) 반면 SysFreeString()  BSTR 을 메모리에서 제거하는 역할을 한다.

BSTR bstr = NULL;
 
  bstr = SysAllocString ( L"Hi Bob!" );
 
  if ( NULL == bstr )
    // 메모리가 부족
 
  // bstr을 마음껏 사용^^
 
  SysFreeString ( bstr );

사실, 스트링 하나 사용하겠다고 이런 일련의 함수를 계속적으로 사용하는 것은 굉장히 피곤하다. 따라서 자연스럽게 메모리 할당, 제거를 자동적으로 해주는 몇몇 랩퍼 클래스들이 생겨나게 됐다. 그에 대해서 뒤에 살펴보기로 하자.

 

Posted by 나비:D
:

백택 (bektekk@yahoo.co.kr)
홈페이지 : http://bektekk.wo.to

문자열(스트링) 전격 분석 2강

본강좌는 코드 프로젝트에 제가 좋아하는 프로그래머인 Michael Dunn의 강좌
The Complete Guide to C++ Strings, Part I 과 II 를 번역한 글입니다.
최대한 의역을 하려고 노력했지만, 이런쪽의 경험이 부족하다 보니 많이 모자란
강좌가 되겠지만, 많은 도움이 되셨으면 합니다.
기타 문의 사항이나 질문은 쪽지나 메일을 이용해 주셨으면 합니다.
본강좌는 제 홈페이지(위의 링크)를 통해서도 보실수 있습니다.

이번강 까지가 좀 지루하겠네요.
다음강 부터는 실제 클래스들을 다루어서 조금은 덜 지루할거라 생각됩니다.

스트링을 조작하기^^

우리는 처음 C를 배울때 부터 SBCS 스트링을 사용하는데 익숙해져 있습니다. strlen() 같은 함수는 아마 처음 C공부하실때 누구나 써보셨을 것이고 아직도 쓰시고 계실겁니다. 또한 초보티를 막 벗을때 char* 타입을 이용해 ++, -- 연산자를 이용해 가면서 문자열 조작들을 해봤을겁니다. 또한 [] 식의 배열표현법으로 캐릭터 하나씩 값을 얻어오는 것에도 이미 익숙하실 겁니다. 이런 일련의 작업들은 SBCS 나 Unicode 스트링에서는 훌륭하게 작동합니다. 왜냐하면 모든캐릭터는 같은 길이이기 때문입니다. SBCS에서는 1바이트 유니코드에서는 2바이트 이기 때문이죠.

하지만, 우리는 DBCS 즉, 2바이트이상의 캐릭터들을 사용하게 된다면 이런 습관들을 반드시 버려야 합니다. 만약 DBCS 스트링을 쓸때에는 반드시 따라야 할 두가지의 룰이 있습니다. 이 룰을 어기면 야근이 버그 잡느라 좋아하는 드라마도 못보시고, 야근 하랴, 머리 빠지랴 고생들이 많으실 겁니다. ^^ (너무 장황하게 늘어놓았네요)

첫째, Lead Byte를 채크해지 않을거면 절대 ++ 포인터 연산을 하지말것

둘째. 절대 절대 -- 포인터 연산을 하지 말것

먼저 두번째 룰을 설명하겠습니다. 왜냐하면 설명하기 쉽기때문에^^. 여러분이 설정화일을 사용하는 프로그램을 작성한다고 가정합시다. 실행하면 그 프로그램은 그 설정파일을 읽어서 작업하겟죠? 만약 프로그램의 경로가 C:\Program Files\MyCoolApp이고, 설정파일은 C:\Program Files\MyCoolApp\config.bin 에 있습니다.

그럼 설정파일의 경로를 얻어오는 함수를 이렇게 작성했다고 가정합니다.

bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];
 
    // 인스톨된 디렉토리를 얻어온다.
    // .....

    // 만약 마지막 캐릭터가 백슬래시가 아니면 추가해 준다.
    // 우선 마지막 캐릭터를 구한다.
    char* pLastChar = strchr ( szConfigFilename, '\0' );
 
    // *** 자 한칸 앞으로 가자 ***
    pLastChar--;  
 
    // 백 슬래시 추가
    if ( *pLastChar != '\\' )
        strcat ( szConfigFilename, "\\" );
 
    // 설정파일 디렉토리 끝에 화일명 추가
    strcat ( szConfigFilename, "config.bin" );
 
    // 리턴
    if ( strlen ( szConfigFilename ) >= nBuffSize )
        return false;
    else
        {
        strcpy ( pszName, szConfigFilename );
        return true;
        }
}

이 루틴은 잘짜여졌습니다. 그러나 특정한 DBCS캐릭터에서는 제대로 작동하지 않을 것입니다. 이유는 다음과 같습니다. 만약 디렉토리 이름이 "C:\디렉토리" 와 같다고 가정해 보면. 그 메모리 구조는 다음과 같게 됩니다. :

 43  3A  5C  83 88  83 45  83 52  83 5C  00
       LB TB  LB TB  LB TB  LB TB  
 C  :  \

 EOS

GetConfigFileName() 함수가 마지막 백슬래시를 확인할때 마지막 0이 아닌 바이트를 확인하는걸 볼수가 있으실 겁니다. (--포인터 연산으로) 그리고 같이 '\\' 인지 == 연산자로 확인했지만 결과적으로는 이 루틴은 잘못된 결과를 리턴합니다.

그럼 무엇이 잘못 되었을가요? 위의 메모리 구조에서 파란색으로 표시한 두 바이트를 살펴보면 백슬래시의 값은 0x5C이고 '리'의 값은 83 5C 입니다. (무언가 눈치 채셨겠죠?.. ^^) 위의 루틴은 '리'의 TB즉 Trail Byte만을 읽고 '\\'와 같은 같으로 간주해 버리게 됩니다.

위의 코드를 수정하려면 --포인터 연산을 제거하고 그에 상응하는 DBCS를 지원하는 함수로 대체 하는 것입니다. 수정한 코드는 아래와 같습니다. :

bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
    char szConfigFilename[MAX_PATH];
 
    char* pLastChar = _mbschr ( szConfigFilename, '\0' );
 
    // 자 이제 제대로 된다.
    // 이전 캐릭터로 이동한다. 이것은 1바이트 일수도 있고
    // 2바이트 일수도 있다.
    pLastChar = CharPrev ( szConfigFilename, pLastChar );
 
    if ( *pLastChar != '\\' )
        _mbscat ( szConfigFilename, "\\" );
 
    _mbscat ( szConfigFilename, "config.bin" );

    if ( _mbslen ( szInstallDir ) >= nBuffSize )
        return false;
    else
        {
        _mbscpy ( pszName, szConfigFilename );
        return true;
        }
}

위의 수정된 루틴은 한 캐릭터 전으로 가기 위해 CharPrev() API 함수를 이용했습니다. 그 한 캐릭터는 위의 예에서와 같이 2바이트를 차지할 수도 있고, 1바이트 일수도 있으나, CharPrev() 함수는 이를 스스로 확인하고 제대로 작동할 것입니다. 따라서 위의 함수는 제대로된 결과 값을 리턴할 것입니다..

이제, 여러분은 위의 1번 규칙을 깨게 될때 일어나는 부작용도 쉽게 생각하실수 있으실 겁니다. 예를 들어, 여러분이 유저가 입력한 파일경로에서 캐릭터 ':' 가 중복되서 나타나는지 아닌지를 확인하는 루틴을 짠다고 생각해 보겠습니다. 그 루틴에서 만약 CharNext() API 함수 대신에 ++ 포인터 연산을 사용하셨다면 잘못된 결과를 초래할 것입니다. 특히 2바이트를 차지하는 한글 한 글자의 Trail Byte가 ':' 와 같게 되는 경우는 100% 잘못된 연산을 하게 될겁니다.

위에서 말한 2번 규칙에 하나를 더하자면:

2a. 절대로 배열 인덱스에 마이너스 연산을 하지 말자.

아래의 코드는 2번 규칙의 예와 굉장히 흡사합니다. 예로, pLastChar 값이 이런식으로 할당되었다면 :

char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];

이 루틴의 결과는 위의 설명드린 --포인터 연산의 부작용예와 똑같은 상황이 벌어집니다. 왜냐 사실 배열 인덱스의 연산도 내부적으로는 포인터 연산으로 처리가 되기 때문이죠.. 헥헥, 꽤나 장황하게 설명된 파트지만 실지 내용은 간단하죠?

다시 strxxx() 와 _mbsxxx() 함수로 돌아가서

이제는 왜 _mbsxxx() 함수시리즈가 필요한지 느끼실 겁니다. strxxx() 계열의 함수는 _mbsxxx() 함수시리즈와는 다르게 DBCS에 대해 전혀 알지 못합니다. 만약strrchr("C:\\디렉토리", '\\') 이런식으로 호출을 하셨다면 그 결과같은 잘못된 값일겁니다. 반면 _mbsrchr() 함수는 마지막의 2바이트 캐릭터를 제대로 인식하고 제대로된 리턴값을 넘겨줄겁니다.

마지막으로 한가지 추가하자면 문자열 길이를 매개변수로 받거나 리턴하는 함수들에는 주의 하실 필요가 있습니다. strlen("스트링") 의 함수는 6을 리턴할 것입니다. 하지만 유니코드 함수 wcslen(L"스트링") 이건 3을 리턴할 겁니다. 주의하게요^^.

Win32 API에서의 MBCS 와 Unicode

두 종류의 API들

아마 눈치 채셨을지도 모르고, 아닐지도 모르지만, 모든 문자열을 다루는 API들은 두 종류로 이루어져 있습니다. 한가지 버젼은 MBCS를 다루고 다른 하나는 유니코드를 다룹니다. 예로, 실제로는 SetWindowText() 라는 함수는 없습니다. 대신 SetWindowTextA()  SetWindowTextW() 라는 두종류의 API가 실제로 존재하는 겁니다. 마지막의 A는 (ANSI의 약어쯤)은 MBCS로 처리하는 함수를 뜻하고, W는 유니코드 버젼을 뜻합니다.

여러분이 프로그램을 빌드 하실때 여러분은 MBCS냐 혹은 유니코드냐를 선택하실 수 있습니다. 만약 VC에서 셋팅을 건드리지 않으셨다면 기본적으로는 MBCS방식으로 빌드될겁니다. 그럼 어떻게 정의되지도 않은 SetWindowText() 라는 함수를 쓸수 있느냐 궁금하시겠죠? winuser.h 헤더파일을 살펴보시면 다음과 같은 일련의 #define문들을 보실 수 있으실 겁니다.:

BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );
BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );
 
#ifdef UNICODE
#define SetWindowText  SetWindowTextW
#else
#define SetWindowText  SetWindowTextA
#endif

MBCS로 빌드 할때에는 UNICODE 가 정의되 있지 않습니다. 따라서 전처리기는 다음으로 해석하게 됩니다.:

#define SetWindowText  SetWindowTextA

따라서 전처리기는 모든 SetWindowText() 라는 문자를 SetWindowTextA() 라고 바꾸게 됩니다. 실제로 SetWindowText() 라는 매크로로 정의된 다른이름의 함수대신 SetWindowTextA 혹은 W 로 끝나는 함수를 쓰실수 있습니다. 물론 그럴일은 거의 없겠지만요..

그럼 기본값으로 유니코드를 사용하는 함수로 싸그리 바꾸고 싶으시다면, VC설정의 preprocessor settings에서 _MBCS 값을 list of predefined symbols에서 제거해 주시고 UNICODE  _UNICODE를 넣어주시면 끝납니다. 주의하실 것은 두개의 값을 모두 적어주셔야 합니다. 어떤해더는 UNICODE라는 것만 사용하고 어떤건 _UNICODE를 사용하기 때문입니다. 그렇지만, 유니코드를 사용하실때 주의 하실게 있습니다. 다음 코드를 살펴보죠:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
 
    SetWindowText ( hwnd, szNewText );

위 코드는 전처리기가 "SetWindowText"를 "SetWindowTextW"로 바꾼후에는 다음과 같습니다.:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
 
    SetWindowTextW ( hwnd, szNewText );

무엇이 잘못되었는지 아시겠나요? 여기서 우리는 유니코드를 취하는 함수에 SBCS 즉 1바이트 케릭터를 전달했습니다. 즉 제대로된 결과를 기대할수 없겠죠? 그 첫번째 해결방법은 모든 스트링에 #define문으로 아래와 같이 정의 하는것입니다.:

HWND hwnd = GetSomeWindowHandle();
#ifdef UNICODE
wchar_t szNewText[] = L"we love Bob!";
#else
char szNewText[] = "we love Bob!";
#endif
 
    SetWindowText ( hwnd, szNewText );

아마 모든 스트링을 이런식으로 정의하다가는 미쳐 버릴겁니다. 다른 직종을 알아보시겠죠? 여기에 대한 진정한 해결책은 TCHAR 입니다.

고맙다 TCHAR!

TCHAR MBCS에서건 유니코드에서건 똑같은 코드를 사용할수 있게하는 캐릭터 타입입니다. 바로 위에서 본 #define 문들의 해결방식을 이미 MS에서는 정의해 TCHAR 라는 방식으로 정의해 놓았습니다. 아래와 같습니다.:

#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif

따라서 TCHAR 는 MBCS로 빌드할때는 char 이고, 유니코드로 빌드할때는 wchar_t 타입이 됩니다. 또한 유니코드 스트링앞에 추가해 주는 L이라는 문자를 위해_T() 라는 마크로도 있습니다.:

#ifdef UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif

## 는 두개의 문자열들을 하나의 문자열로 만들어 주는 전처리기 연산자 입니다. 여러분은 이 글을 보고 나면 이제 스트링을 선언 할때면 언제든지 _T 마크로를 사용하셔야 합니다. 이 마크로는 유니코드 문자열 앞에는 L문자를 추가해 줍니다.

TCHAR szNewText[] = _T("we love Bob!");

SetWindowTextA/W 를 편하게 왔다갔다 할수 있게 해주는 정의들이 있는것 처럼, strxxx()  _mbsxxx() 류의 스트링 처리 함수들 사이를 편하게 변경시킬수 있는 정의 들도 있습니다. 예를 들면, 여러분은 _tcsrchr 마크로를 strrchr() 이나 _mbsrchr() 혹은 wcsrchr()를 쓰는 대신 쓰실수 있습니다. _tcsrchr 함수는SetWindowText 함수가 그러하듯 MBCS환경이냐 혹은 유니코드 환경이냐에 따라서 적절히 치환될 것입니다.

TCHAR 를 취하는 함수는 비단 strxxx() 계열의 함수만은 아닙니다. 예를 들면 _stprintf (sprintf()  swprintf()를 치환) 이나 _tfopen (fopen()  _wfopen()를 치환)과 같은 함수들도 있습니다. 이와 관련된 함수들의 전체 리스트는 MSDN의 "Generic-Text Routine Mappings" 부분을 참고하시기 바랍니다.

문자열 그리고 TCHAR 타입들

타입 MBCS환경에서 유니코드 환경에서
WCHAR wchar_t wchar_t
LPSTR 0으로 끝나는 char형의 문자열(char*) 0으로 끝나는 char형의 문자열 (char*)
LPCSTR 0으로 끝나는 const char형의 문자열 (const char*) 0으로 끝나는 const char형의 문자열 (const char*)
LPWSTR 0으로 끝나는 유니코드형의 문자열 (wchar_t*) 0으로 끝나는 유니코드형의 문자열 (wchar_t*)
LPCWSTR 0으로 끝나는 const 유니코드형의 문자열 (const wchar_t*) 0으로 끝나는 const 유니코드형의 문자열 (const wchar_t*)
TCHAR char wchar_t
LPTSTR 0으로 끝나는 TCHAR형의 문자열 (TCHAR* -> char*) 0으로 끝나는 TCHAR형의 문자열 (TCHAR*->wchar_t*)
LPCTSTR 0으로 끝나는 const TCHAR형의 문자열 (const TCHAR*) 0으로 끝나는 const TCHAR형의 문자열 (const TCHAR*)

 

언제 TCHAR 와 유니코드를 사용할 것인가

이제 여러분은 유니코드 없이도 잘 살아 왔는데 왜 유니코드를 사용하여야 하는가 의문이 들 것입니다. 유니코드 방식을 쓰면 득이 될 경우는 세가지가 되겠습니다.:

  1. 프로그램이 오직 Windows NT 환경에서만 돌아간다.
  2. 프로그램이 MAX_PATH 보다 긴 파일명의 문자열을 사용한다.
  3. 프로그램이 Window XP등에서 소개된 오직 유니코드만 받는 함수를 사용한다.

대부분의 유니코드함수는 Windows 9x시리즈에는 구현되 있지 않습니다. 프로그램이 9x시리즈에서도 돌아가야 한다면 유니코드는 좋은 선택이 아닐겁니다. (Microsoft Layer for Unicode 라는 9x에서도 유니코드를 지원하는 라이브러리가 있긴 합니다.) 그렇지만, NT는 내부적으로는 모두 유니코드를 사용하기 때문에 유니코드 API가 아마 약간의 성능향상에 도움이 될것입니다. MBCS방식의 스트링을 NT환경에서 사용하면 운영체제는 내부적으로 그 스트링을 유니코드로 변환한 다음 그에 상응하는 유니코드 함수를 호출할 것입니다. 결과가 리턴되면 다시 MBCS방식으로 전환한 다음 리턴해 줄것입니다. 이 절차는 상당히 최적화 되있겠지만, 아마 유니코드를 직접쓸때보다는 성능의 아주 약간일지도 모르지만 감소는 피할수 없을 겁니다.

NT는 또한 아주 긴 파일명을 지원합니다. (MAX_PATH 보다 긴 파일명) 그러나 유니코드가 쓰일 때만 입니다. 하지만 무엇보다도 가장 큰 장점은 모든 언어 예를 들면 중국어, 영어 , 일어, 한국어 를 똑같은 방식으로 처리해 줄수 있다는 것일 겁니다.

마지막으로, Windows 9x 시리즈를 마지막으로 MS는 MBCS API방식을 멀리하고 유니코드 방식으로 전향하고 있습니다.  예를들면, SetWindowTheme() 이라는 API 함수는 오직 유니코드 스트링 만을 받습니다. 상응하는 MBCS버젼이 없습니다. 유니코드만을 사용하는 것은 이제 유니코드와 MBCS사이의 왔다갔다 해야하는 귀찮음을 얻에 줄겁니다.^^

그리고 지금 당장 유니코드를 사용하지 않으실 지라고, 여러분은 반드시 TCHAR 와 그와 관련된 함수들을 써주셔야 미래를 위해 좋으실 겁니다. DBCS를 보다 안전하게 사용하는것 못지않게 나중에 유니코드로 프로그램을 바꾸실때 단지 셋팅하나 바꿔주면 만사 오케이 이기 때문이죠^^

Posted by 나비:D
:

백택 (bektekk@yahoo.co.kr)
홈페이지 : http://bektekk.wo.to

문자열(스트링) 전격 분석1강

본강좌는 코드 프로젝트에 제가 좋아하는 프로그래머인 Michael Dunn의 강좌
The Complete Guide to C++ Strings, Part I 과 II 를 번역한 글입니다.
최대한 의역을 하려고 노력했지만, 이런쪽의 경험이 부족하다 보니 많이 모자란
강좌가 되겠지만, 많은 도움이 되셨으면 합니다.
기타 문의 사항이나 질문은 쪽지나 메일을 이용해 주셨으면 합니다.
본강좌는 제 홈페이지(위의 링크)를 통해서도 보실수 있습니다.

사실 이글은 Part I/II 두 강의로 구성이 되있지만 제 역량이 모자라는 관계로
더 잘게 쪼개 여러강좌로 재구성하겠습니다.
 

소개

프로그래밍 작업을 하면서 TCHAR, std::string, BSTR등과 같은 많은 문자열관련 데이터타입을 보셨을 겁니다. 또한 _tcs로 시작하는 마크로들도 많이 보셨을 겁니다. 아마 특히 초보분들은 많이들 어려워 하셨을겁니다. 이 강좌는 각각의 문자열 타입을 정리해 보고, 각각의 목적을 소개할 겁니다. 더 나아가 간단한 사용법과 각각의 데이터 타입으로 어떻게 변환할수 있는지도 살펴보겠습니다.

먼저 세가지 종류의 케릭터 인코딩 타입에 대해 구체적으로 알아 보겠습니다. 여러분들은 그 각각의 문자열들이 내부적으로 어떻게 처리되는지 반드시 알아야 할것입니다. 스트링은 캐릭터들의 배열이라는 사실을 이미 알고 계실지라고, 이번강좌는 여러분께 도움이 많이 될것입니다. 또한 이번 강좌를 통해 스트링과 관련된 많은 자료구조(클래스, 구조체)에 대해서도 더 확실히 아시게 될겁니다.

그다음, 스트링 클래스들에 대해 다룰것입니다. 언제 어떤 클래스를 쓰는게 좋은지, 또 각각 어떻게 변환할수 있는지 살펴 볼것입니다.

캐릭터들의 기본 - ASCII, DBCS, Unicode

모든 스트링 클래스들은 사실상 그 근간을 C-스타일 스트링에 두고 있습니다. 다들 아시다 시피 C-스타일 스트링은 캐릭터의 배열로 구성되어 있습니다. 그럼 먼저 캐릭터 타입에 대해 다루도록 하겠습니다. 현재 우리가 쓰고있는 인코딩방법에는 세가지가 있습니다. 그 중 첫째로 single-byte character set, 혹은 SBCS 는 모든 케릭터가 정확히 한바이트를 차지합니다. C의 데이타 타입인 char형을 생각하시면 됩니다. 많이들 알고계실 ASCII 는 SBCS의 가장 대표적인 예입니다. 제로 바이트 즉 '\0' 값이 마지막에 반드시 존재하며, 그것은 문자열의 끝을 나타내게 냅니다.

둘째로는 multi-byte character set, 혹은 MBCS를 들수 있겠습니다. MBCS는 2바이트가 필요한 캐릭터(한글, 일본어, 중국어같은것들)은 2바이트로 1바이트만 써도 되는것들(영어 같은것들)은 1바이트로 표현을 합니다. 사실상 3바이트가 필요한 문자열은 3바이트로 표현을 하겠지만, 그런 문자열들이 지구상에 현재 없죠? 아마도. 윈도우즈에서는 single-byte characters  double-byte characters 이렇게 두가지 MBCS 인코딩방식이 쓰입니다. 따라서 윈도우즈에서 지원하는 가장긴 바이트의 캐릭터는 2바이트가 됩니다. 그래서 MBCS는 종종 double-byte character set, 혹은 DBCS 와 같은 의미로 사용되기도 합니다.

DBCS 인코딩방식에서는, 어떤 특정한 값이 2바이트인지를 나타내게 됩니다. 왜냐하면 어떻 케릭터가 1바이트인지 2바이트인지를 구별할수 있는 방법이 필요하기 때문입니다. 예를 들면 Shift-JIS 인코딩에서는 (일본에서 가장 많이 사용되는 인코딩 방식) 0x81-0x9F 와 0xE0-0xFC 사이의 값은 캐릭터가 2바이트라는것을 나타냅니다. 이런 값들을 "lead bytes" 라고 부르고 그 값은 항상 0x7F 보다 큽니다. "lead bytes" 다음에 나오는 바이트는 "trail byte"라고 부릅니다.DBCS에서는 trail byte는 0이 아닌 어떤값을 가질 수 있습니다. SBCS에서와 같이 DBCS방식에서도 '\0' 값을 가지는 한 바이트가 문자열의 마지막을 나타냅니다.

마지막은 Unicode 입니다. Unicode는 모든 캐릭터를 2바이트로 나타내자는 표준 인코딩방식입니다. 유니코드 캐릭터는 종종 wide characters라고도 불리는데요, 이는 1바이트 캐릭터들 즉, SBCS방식보다 더 많은(넓은) 공간을 차지하기 때문입니다. 유니코드는 MBCS와는 다르다는것을 주의 하세요. 가장 큰 차이점을 MBCS방식에서는 한 캐릭터가 1바이트 일수도 2바이트일수도 심지어 3바이트일수도 있습니다. 하지만 유니코드에서는 모든 캐릭터들이 2바이트를 차지하게 됩니다. 또하나의 차이점은 유니코드는 MBCS, SBCS에서와는 다르게 문자열의 끝은 "\0\0" 이런식으로 제로바이트 두개로 표시합니다.

SBCS는 주로 서유럽언어, 대표적으로 영어, 에서 주로 사용됩니고 ASCII표준으로 정의되어있습니다. MBCS는 동아시아 중동지역 언어를 나타내기위해 주로 사용됩니다. (한국, 일본, 중국이 대표적이죠) 유니코드는 COM과 윈도우즈NT에서 내부적으로 사용하고 있습니다.

아마 여러분들은 SBCS 즉, single-byte 캐릭터에는 이미 익숙하실 겁니다. char 타입으로 영문을 사용하실때 이미 여러분은 SBCS를 사용하고 계신겁니다. char타입으로 한글을 사용하신다면 Double-byte 타입 즉, DBCS를 사용하시는 겁니다. 하지만 그와는 다르게 유니코드에서는 wchar_t 타입을 사용합니다. 유니코드 문자열은 C/C++에서 L이라는 문자로 SBCS나 MBCS와는 다르다는 것을 표시해 줍니다.

  wchar_t  wch = L'1';      // 2 bytes, 0x0031
  wchar_t* wsz = L"Hello";  // 12 bytes, 6 wide characters

캐릭터들이 메모리에 저장되는 방식

1바이트 스트링은 차례차례 1바이트씩 저장이 됩니다. 마지막은 제로바이트 '\0'으로 문자열의 끝을 말해줍니다. 따라서 예를 들어보면"Bob" 이라는 문자열은 이와 같은 방식으로 저장됩니다.

 42   6F  62   00 

B

o

b

문자열의 끝

유니코드 방식에서, L"Bob"은 이렇게 저장이 됩니다.

 42 00   6F 00   62 00   00 00 

B

o

b

문자열끝 두개의 제로 바이트

위에서 보시다 시피 캐릭터 0x0000 이 문자열 끝을 나타냅니다.

DBCS 스트링은 겉보기에 SBCS방식과 흡사하지만 그 차이점이 있습니다. 그에 대해서는 뒤로 미루기로 하죠. 문자열 "스트링" 은 아래와 같은 방식으로 저장됩니다.(여기서 LB는 Lead Byte 그리고 TB는 Trail Byte를 뜻합니다.):

 93 FA  96 7B  8C EA  00 
 LB TB  LB TB  LB TB  EOS

 문자열 끝

"스"라는 캐릭터는 WORD 값 0xFA93 이런식으로 생각하시면 안됩니다. 두 개의 1바이트값 93  FA 의 순서로 "스"라는 캐릭터를 나타내는 겁니다. 따라서 intel계열이 아닌 big-endian 방식이 CPU에서도 그 순서는 같습니다.

스트링 처리함수의 사용

strcpy(), sprintf(), atol()등과 같은 C문자열 처리함수들은 이미 많이 보셨을 겁니다. 중요한 점은 이러한 함수들은 반드시 1바이트 스트링에서만 사용되어져야 한다는 겁니다. 표준라이브러리는 또한가지의 다른 함수셋을 가지고 있습니다. 이 함수들은 유니코드 용인데요, wcscpy(), swprintf(), _wtol() 등의 함수들이 있습니다. 대략 함수중간에 자주보이는 w 는 유니코드를 뜻하죠, wider 캐릭터에서 w를 땃겠죠?

MS는 또한 DBCS를 지원하는 표준라이브러리를 추가했습니다. strXXX()류의 함수는 _mbsXXX()의 함수와 대응됩니다. 만약 여러분의 프로그램이 2바이트 언어권에서 사용된다면 반드시 _mbs로 시작하는 문자열 함수를 사용해야 합니다. 사실 우리 한국사람은 반드시 _mbs류의 함수를 쓰는게 정신건강에 좋겠죠? 왜냐, _mbs함수는 SBCS방식의 문자열도 정확히 처리해 주기 때문입니다. 왜냐하면 MBCS방식에서는 1바이트 캐릭터도 존재하기 때문에 SBCS방식의 문자열이 정확히 처리될수 있는겁니다.

그럼 전형적인 유니코드 스트링을 보면서 왜 여러종류의 스트링 처리함수들이 필요한지 얘기해보겠습니다. 전에 살펴보았던 유니코드 스트링 L"Bob" 입니다.

 42 00   6F 00   62 00   00 00 

B

o

b

EOS

만약 위의 문자열을 strlen() 함수에 사용하면 어떤 문제가 있을까요? strlen()함수에서는 처음 42값을 가지는 한바이트를 읽고 그 다음 00값의 한바이트를 읽겠죠? 하지만 이 00값의 바이트는 문자열의 끝을 나타냅니다. 따라서 리턴값은 1을 돌려줄 겁니다. 분명 잘못된 결과죠? 음. 반대의 상황은 더욱 치명적입니다. "Bob" 이라는 SBCS방식의 문자열을 wcslen() 함수(유니코드용)에 넘겨준다고 생각해 봅시다. "Bob"는 메모리에 42 6F 62 00 이렇게 저장이 됩니다. 하지만 wcslen() 함수에서는 두바이트씩 읽어 가면서 "0000"이렇게 두바이트가 모두 0인 값을 찾아값니다. "Bob"에 경우 먼저 42 6F를 읽겠죠? 그다음 62 00 을 읽을 것이고 이런식으로 00 00을 찾을때까지 여기저기 들쑤시면서 찾아 나갈 겁니다. 예상과 다른 결과가 나올것은 자명하죠.

strxxx()  wcsxxx() 통해 스트링처리함수들에 대해 간략히 얘기해 보았습니다. 그럼 strxxx()  _mbsxxx() 의 경우는 어떠할가요? 이 둘의 차이도 정말 중요합니다. 반드시 적절한 방식으로 사용되어 져야 합니다. 이에 대해서는 뒤에서 다루기로 하겠습니다.

Posted by 나비:D
:

[MFC] 핵심 TIP

2009. 10. 29. 14:29

출처 : http://tong.nate.com/navy9370/30050194
1. DC얻기

  CClientDC dc(this);



2. Client 영역 구하기

  GetClientRect(&rect);

  WM_SIZE 메시지발생후 cx,cy 사용



3. 문자열 사각형안에 그리기

  pDC->DrawText(문자열,사각형,Style);

  Style: DT_BOTTOM - 문자열을 사각형 맨아래줄에배열 반드시 DT_SINGLELINE과 함께사용

        DT_CENTER - 문자열을 가로중앙에 배치

        DT_VCENTER - 문자열을 세로중앙에 배치

        DT_LEFT,RIGHT - 문자열을 좌,우로 배치

        DT_SINGLELINE - 문자열을 한줄로만 쓴다



4. Brush 사용법

  CBrush brushname(RGB(red,green,blue)); //브러쉬 생성

  CBrush *oldBrush=pDC->SelectObject(&brushname); //이전Brush 저장, 새로운 Brush 선택

  pDC->SelectObject(oldBrush); //원래의 브러쉬로 반환



5. Pen사용법

  CPen pen(Pen Style,RGB(red,green,blue)); //브러쉬생성

//Style: PS_SOLID,PS_DASH,PS_DOT,PS_DASHDOT,PS_GEOMETRIC,PS_COSMETRIC - 펜종류

        PS_ENDCAP_ROUND,PS_ENDCAP_SQUARE - 펜끝을 둥글게,각지게 설정

  CPen *oldPen=pDC->SelectObject(&pen); //이전Pen저장, 새로운 Pen설정

  pDC->SelectObject(oldPen); //펜반환



6. 화면다시그리기

  View Class에서 - Invalidate(TRUE) : 화면을 지우고다시그린다

                    Invalidate(FALSE) : 화면을 덮어씌운다

  UpdateAllViews(NULL);  // Doc Class에서 View 의 OnDraw 호출

  RedrawWindow();



7. 메시지,함수 수동으로 넣기 (EX)버튼클릭함수넣기

  헤더파일의 AFX_MSG_MAP 부분에 함수를 정의

  EX) afx_msg void funcName();

  .cpp파일의 AFX_MSG 부분에 메시지를 추가한다

  EX) ON_BN_CLICKED(ID_NAME,funcName)...

  ID 등록:  View 메뉴의 Resource Symbol 에 들어가서 메뉴 ID 를 등록해준다..

  .cpp파일의 맨아래에서 함수를 정의한다

  EX) void CClass::funcName() { ... }



8. 마우스커서 바꾸기

  리소스탭에서 커서를 그리고 저장한뒤 ID값은 준다음

  SetCapture(); //커서의입력을 클라이언트영역을 벗어나더라도 받아낸다

  SetCursor(AfxGetApp()->LoadCursor(nIDResource));

  //APP클래스의 LoadCursor View의 SetCursor 사용

  ReleaseCapture(); //SetCursor()상태를 해제한다



9. 색상표 사용하기

  CColorDialog dlg;

  if(dlg.DoModal()==IDOK) //Dialog 를 띄운후 OK버튼을누르면 실행할부분

  MemberFunc: GetColor() //선택된 색상을 받아온다 return 형은 COLORREF 형



10. 팝업메뉴 만들기

  CMenu menu; //메뉴 객체생성

  CMenu *pmenu; //메뉴 포인터생성

  menu.LoadMenu(IDR_MAINFRAME); //메뉴를 불러온다

  pmenu=menu.GetSubMenu(3); //메뉴의 3번째 메뉴를 가져온다

  menu.CheckMenuItem(ID_MENU,m_kind==ID_MENU ? MF_CHECKED : MF_UNCHECKED);

  //메뉴 체크하기 (메뉴 ID, ID 체크조건)

  pmenu->TrackPopupMenu(TPM_LEFTALIGN,point.x,point.y,this)  //(TMP_Style,x좌표,y좌표,hWnd) 메뉴 띄우기



  *주의사항*

    [안내]태그제한으로등록되지않습니다-OnContextMenu(CWnd* pWnd, CPoint point)  //여기서 point 는 스크린 기준이고,

    OnRButtonDown(UINT nFlags, CPoint point)  //여기서 point 는 클라이언트 기준이다!



11. 클라이언트 포인터를 스크린 포인터로 변경

  ClientToScreen(&point);



12. 그림판기능

         if(m_flag==FALSE)  return;   //m_falg=그리기 기능 참,거짓설정  그리기 아니면 빠져나간다

        CClientDC dc(this);

        CPen myPen(PS_SOLID,m_width,m_color);

        CPen *pOldPen=dc.SelectObject(&myPen);

        switch(m_shape)

        {

        case ID_FREELINE: //자유선그리기

                dc.MoveTo(m_oldpt.x,m_oldpt.y); //지난포인터부터

                dc.LineTo(point.x,point.y); //새포인터까지 그린다

                break;

        case ID_RECT: //사각형그리기

                dc.SetROP2(R2_NOTXORPEN);

                dc.Rectangle(m_spt.x,m_spt.y,m_oldpt.x,m_oldpt.y);  //지워지는 효과

                dc.Rectangle(m_spt.x,m_spt.y,point.x,point.y); //그려지는 효과

                break;

        case ID_ELLIPSE: //원그리기

                dc.SetROP2(R2_NOTXORPEN);

                dc.Ellipse(m_spt.x,m_spt.y,m_oldpt.x,m_oldpt.y);  //지워지는 효과

                dc.Ellipse(m_spt.x,m_spt.y,point.x,point.y); //그려지는 효과

                break;

        case ID_LINE: //선그리기

                dc.SetROP2(R2_NOTXORPEN);

                dc.MoveTo(m_spt.x,m_spt.y); //시작점부터

                dc.LineTo(m_oldpt.x,m_oldpt.y); //지난점까지 그은선을 지운다

                dc.MoveTo(m_spt.x,m_spt.y); //시작점부터

                dc.LineTo(point.x,point.y); //새로운점까지 그린다

                break;

        }

        m_oldpt=point;  //바로이전값 보관

        dc.SelectObject(pOldPen); //펜 반환


13. MessageBox

  AfxMessageBox() -> 전역함수를 이용하영 메세지 박스를 출력한다.   //어디서든지 사용할수 잇다

  int CWnd::MessageBox("메세지","창제목","아이콘|버튼(상수값)");   //View클래스에서 사용한다

  아이콘 상수값  MB_IC[안내]태그제한으로등록되지않습니다-xxONERROR, MB_ICONWARNING, MB_ICONQUESTION,MB_ICONINFOMATION

                MB_SYSTEMMODAL //시스템모달 대화창 닫기전에 다른작업 못함

                MB_APPLMODAL //응용모달

  버튼 상수값    MB_OK, MB_OKCANCEL, MB_YESNO



14. OS 컨트롤

        ExitWindowEx(EWX_SHUTDOWN,NULL); //Shut Down

        ExitWindowsEx(EWX_FORCE,0); //강제종료

        ExitWindowsEx(EWX_LOGOFF,0); //로그오프

        ExitWindowsEx(EWX_POWEROFF,0); //Shut Down -> Turn Off

        ExitWindowsEx(EWX_REBOOT); //Shut Down -> Reboot



15. DialogBox 메시지 교환

        UpdateData(FALSE); // 컨트롤에 멤버변수의 내용을 표시해준다

        UpdateData(TRUE);  // 컨트롤 내용을 다이얼로그 클래스의 멤버변수로 저장



16. 자료변환

        atoi,itoa - int <=> ASCII(char) 변환

        str.Format(" %d %d",x,y); // int형을 문자열로 변환

        atol,ltoa - ASCII <=> long 변환

        atof - ACSII => float 변환

        fcvt,gcvt  - 실수를 text로 변환

        LPtoDP, DPtoLP - 장치좌표 <=> 논리좌표 변환



17. CEdit Class 사용하기

  CEdit e_str.SetSel(int StartChae, int EndChar); //처음문자부터 마지막까지 블록 지정

  CEdit e_str.GetSel(int SChar,int EChar); //블럭 지정한 처음문자와 마지막문자 받기

  CString str=m_str.Mid(SChar,EChar-SChar); //블럭지정한 부분을 가져온다


18. 컨트롤과 자료교환

  SetDlgItemText(컨트롤 ID,문자열) //컨트롤에 문자열을 넣는다

  GetDlgItemText(컨트롤 ID,문자열) //컨트롤의 내용을 문자열에 넣는다

  GetDlgItem(컨트롤 ID); //컨트롤의 주소를 가져온다


19. 상태바조작

  CMainFrame 생성자 위에

  static UINT indicators[] = //이안에 새로운 ID를 넣고 그 ID의 갱신핸들러를 만든다음 코딩

  pCmdUI->SetText("표시할내용“);



20. 수동으로 Bitmap 컨트롤 사용하기

  CStatic bitmap; //bitmap 컨트롤변수

  bitmap.SetBitmap(CBitmap m_bitmap); //컨트롤에 비트맵지정

  GetDlgItem(IDC_BITMAP)->ShowWindow(SW_SHOW,HIDE);  // 그림을 보이거나 숨긴다.

 

21. 응용프로그램 실행하기

  WinExec("프로그램경로“,SW_SHOW,HIDE); //응용프로그램실행,경로는 \\로 구분한다



22. Bitmap 사용하기

  CBitmap bitmap.LoadBitmap(IDC_BITMAP); //비트맵객체에 비트맵지정

  CDC memDC; //그림그릴 메모리DC생성

  MemDC.CreateCompatibleDC(pDC); //화면 DC와 메모리 DC 호환 생성

  CBitmap *pOldBitmap=MemDC.SelectObject(&m_bitmap); //메모리에 그림을그린다.

  pDC->BitBlt(int x, int y,int Width, int Height, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop);

//BitBlt(그림x좌표,그림y좌표,그림넓이,그림높이,그림그려진메모리DC,그림시작x좌표,그림시작y좌표,스타일);

  pDC->StretchBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop )

//StretchBlt(그림x좌표,그림y좌표,그림넓이,그림높이,그림그려진메모리DC,그림x좌표,그림y좌표,메모리그림넓이,메모리그림높이,스타일);

MemDC.SelectObject(pOldBitmap); // 메모리DC반환



23. Font 바꾸기

  CFontDialog dlg; //폰트다이얼로그 생성

  LOGFONT m_logFont; //폰트받을변수선언

  if(dlg.DoModal()==IDOK) //폰트다이얼로그표시

  {dlg.GetCurrentFont(&m_logFont)} //선택된 폰트받기

  OnDraw()

   CFont newFont,*pOldFont; //폰트 객체 만들기

   newFont.CreateFontIndirect(&m_logFont); //폰트 생성

   pOldFont=(CFont *)pDC->SelectObject(&newFont); //폰트 선택

   OnCreate()

   CClientDC dc(this); //DC 생성

   CFont *pFont=dc.GetCurrentFont();        //클라이언트 영역의 폰트를

   pFont->GetLogFont(&m_logFont); //로그폰트 멤버값으로 지정



24. Font 만들기

         LOGFONT logfont; //폰트를 만든다

        logfont.lfHeight=50;               //문자열 높이

        logfont.lfWidth=0;                 //너비

        logfont.lfEscapement=0;            //문자열기울기

        logfont.lfOrientation=0;             //문자개별각도

        logfont.lfWeight=FW_NORMAL;     //굵기

        logfont.lfItalic=TRUE;             //이탤릭

        logfont.lfUnderline=TRUE;  //밑줄

        logfont.lfStrikeOut=FALSE; //취소선

        logfont.lfCharSet=HANGUL_CHARSET; //필수

        logfont.lfOutPrecision=OUT_DEFAULT_PRECIS;              

        logfont.lfClipPrecision=CLIP_DEFAULT_PRECIS;      //가변폭폰트 고정폭폰트

        logfont.lfPitchAndFamily=DEFAULT_PITCH|FF_SWISS; //글꼴이름

        strcpy(logfont.lfFaceName,"궁서체");

        CClientDC dc(this);

        CFont newFont; //폰트객체생성

        newFont.CreateFontIndirect(&logfont); //폰트지정

        CFont *pOldFont=dc.SelectObject(&newFont); //폰트선택

        dc.TextOut(100,100,m_text);

        dc.SelectObject(pOldFont); //폰트반환



25. Font 만들기 2

  CFont newFont;

  newFont.CreateFont( int nHeight, int nWidth, int nEscapement, int nOrientation, int nWeight, BYTE bItalic, BYTE bUnderline, BYTE cStrikeOut, BYTE nCharSet, BYTE nOutPrecision, BYTE nClipPrecision, BYTE nQuality, BYTE nPitchAndFamily, LPCTSTR lpszFacename );

 CFont *pOldFont=dc.SelectObject(&newFont);



26. ComboBox 사용하기

  CComboBox combo; //콤보박스 선언

  combo.Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );

  //Style - WS_CHILD|WS_VISIBLE

  int n=combo.GetCurSel(); //선택된 아이템의 index를 가져온다

  combo.AddString("문자열“); //문자열을 추가한다

  combo.GetLBText(n,str); //n번째 아이템을 str에 저장



27. Spin 사용하기

  Spin은 바로앞의 Tab Order에 따라 붙는다

  m_spinr.SetRange(1900,3000); //스핀 범위 지정

  m_spinr.SetPos(m_nYear); //스핀 위치 지정



28. CTime사용하기

  CTime time; //시간객체생성

  time=CTime::GetCurrentTime(); //현재시간을 저장

  time.GetYear(),time.GetMonth();,time.GetDay(),time.GetHour(),time.GetMinute(),time.GetSecond()



29. CListBox 메소드

  AddString("문자열");             //리스트에 문자열 추가

  DeleteString(index);             //리스트에서 항목 삭제

  GetCount()                     //전체 항목 갯수를 얻는다.

  GetSelcount()                   //선택된 항목 갯수 리턴

  GetSel()                       //선택된 것인지 아닌지를 리턴한다 -> 양수 = TRUE , 음수 => FALSE

  GetText(int index,문자열변수)     //index 번째 문자열을 문자열 변수에 넣는다

  FindStringExact(문자열)          //지정 문자열의 index 값 리턴 -> 없으면 리턴값 LB_ERR 반환

  FindString("a")                 //"a"로 시작하는 항목을 모두 찾는다.

  ResetCountent()                 //모든 내용을 지운다.



30. 파일입출력

 프로젝트생성시 Step4 => Advanced => 저장파일확장자지정

 .h 파일에       DECLARE_SERIAL(CSawon) //이 클래스를 저장,로드가능한 클래스로 쓰겟다는 선언

 .cpp 파일에     IMPLEMENT_SERIAL(CSawon,CObject,1) //이거를 해야 저장이 가능하다

void CFileioDoc::Serialize(CArchive& ar)

        if (ar.IsStoring())  //저장하기

        {ar<

        else    //열기

        {ar>>m_shape; //불러올걸 쓴다. 읽을때도순서대로읽어야한다}



31. MicroSoft FlexGrid 사용하기!

        CMSFlexGrid m_Grid; //FlexGrid 컨트롤 변수

        CString strTitle[]={"고객코드","고객성명","고객포인트","신장","몸무게","고객등급","BMT지수","판정결과"};

        // Grid 의 제목에 넣을문자배열

        int Width[]={900,900,1100,800,800,900,1000,900};

        // Grid 의 열넓이 지정할 배열

        m_Grid.SetRows(m_cnt+2); //전체행수 지정

        m_Grid.SetCols(8); //전체열수 지정

        m_Grid.Clear(); //지우기

        m_Grid.SetFixedCols(0); //고정열은 없다.

        m_Grid.SetRow(0); // 행선택

        for(int i=0;i<=7;i++)

        {

                m_Grid.SetColWidth(i,Width[i]); //열 넓이 설정

                m_Grid.SetCol(i); //열 선택

                m_Grid.SetText(strTitle[i]); // 선택된행, 선택된열에 Text 를 넣는다

        }



32. 4대 Class간 참조

//각각 헤더파일 include

#include "MainFrm.h" //메인프레임 헤더파일

#include "ClassDoc.h"   //Doc클래스 헤더파일

#include "ClassView.h" //View를 include 할때는 반드시 Doc 헤더파일이 위에잇어야한다

#include "Class.h" //APP Class 의 헤더파일



void CClassView::OnMenuView() //뷰클래스

        CClassApp *pApp=(CClassApp *)AfxGetApp();   //View -> App
        CMainFrame *pMain=(CMainFrame *)AfxGetMainWnd();  //View -> MainFrm

        CClassDoc *pDoc=(CClassDoc *)pMain->GetActiveDocument(); //View -> MainFrm -> Doc

        CClassDoc *pDoc=(CClassDoc *)GetDocument();                     //View -> Doc



 //MainFrame 클래스

        CClassView *pView=(CClassView *)GetActiveView();  //MainFrm -> View

        CClassDoc *pDoc=(CClassDoc *)GetActiveDocument();  //MainFrm -> Doc

        CClassApp *pApp=(CClassApp *)AfxGetApp(); //MainFrm -> App



//Doc 클래스

        CClassApp *pApp=(CClassApp *)AfxGetApp(); //Doc -> App

        CMainFrame *pMain=(CMainFrame *)AfxGetMainWnd(); //Doc -> MainFrm

        CClassView *pView=(CClassView *)pMain->GetActiveView(); // Doc -> MainFrm -> View

        CClassView *pView=(CClassView *)m_viewList.GetHead();      // Doc -> View



//App 클래스

        CMainFrame *pMain=(CMainFrame *)AfxGetMainWnd(); //App -> MainFrm

        CClassView *pView=(CClassView *)pMain->GetActiveView(); //App -> MainFrm -> View

        CClassDoc *pDoc=(CClassDoc *)pMain->GetActiveDocument(); //App -> MainFrm -> Doc



33. ToolBar 추가하기

  CMainFrame 으로 가서 멤버변수 추가

        CToolBar m_wndToolBar1;

  OnCreate 로 가서 다음 내용을 추가해준다 (위의 toolbar 부분을 복사하고 이름만 바꾸면 된다.3군데..)

  if (!m_wndToolBar1.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP

                | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||

                !m_wndToolBar1.LoadToolBar(IDR_TOOLBAR1))

        {

                TRACE0("Failed to create toolbar\n");

                return -1;      // fail to create

        }



  그 함수내에서 //TODO 아래에 내용추가..역시..복사해서 이름만 바꾸면 된다.

        m_wndToolBar1.EnableDocking(CBRS_ALIGN_TOP|CBRS_ALIGN_BOTTOM);

        //DockControlBar(&m_wndToolBar1);   <= 이부분 대신..

        이거를 넣는다..

        CRect toolRect; //툴바 영역을 얻을 사각형

        this->RecalcLayout(); //현상태의 Client 영역을 구해서 저장한다

        m_wndToolBar.GetWindowRect(&toolRect); //툴바영역을 저장한다

        toolRect.left+=1; //사각형의 왼쪽을 1Pixel 줄인다

        DockControlBar(&m_wndToolBar1,AFX_IDW_DOCKBAR_TOP,&toolRect); //ToolRect에 툴바를 붙인다

        return 0;



34. ToolBar에 ComboBox붙이기

        CComboBox m_combo; //객체생성

        ID 등록 => view 메뉴 => resource symbol => new => ID_COMBO

  oncreate 에 내용 추가 (콤보를 만들고 표시하는 내용)

        m_wndToolBar.SetButtonInfo(10,IDC_COMBO,TBBS_SEPARATOR,150);

        //툴바의 10번째버튼을 편집한다

        CRect itemRect; //콤보를넣을 사각형을 만든다

        m_wndToolBar.GetItemRect(10,&itemRect); //툴바의 10번째 버튼을 사각형에 넣는다 
        itemRect.left+=5; //앞여백

        itemRect.right+=5; //뒤여백

        itemRect.bottom+=100; //콤보가열릴 공간확보

        m_combo.Create(WS_CHILD|WS_VISIBLE|CBS_DROPDOWN,itemRect,&m_wndToolBar,IDC_COMBO);

        //콤보박스를 툴바에 붙여준다

        m_combo.AddString("이름"); //내용추가

        m_combo.SetCurSel(0); //셀 선택



35.  Toolbar에 수동으로넣은 ComboBox 사용하기

  afx_msg void [안내]태그제한으로등록되지않습니다-xxOnSelectCombo(); //원형

  ON_CBN_SELCHANGE(IDC_COMBO,[안내]태그제한으로등록되지않습니다-xxOnSelectCombo) //메세지맵에 추가

        CMainFrame *pMain=(CMainFrame *)GetParent(); //메인프레임 주소참조

        CComboBox *pCom=(CComboBox *)(pMain->m_wndToolBar.GetDlgItem(IDC_COMBO));

        //콤보박스의 주소를 가져온다, 접근할 때 메인프레임 -> 툴바 -> 콤보박스 의 순서로 가야한다

        int n=pCom->GetCurSel(); //현재선택된 셀의 인덱스를 가져온다

        if(n==CB_ERR) return; //선택된셀이 없으면 중지한다

        CString str;

        pMain->m_combo.GetLBText(n,str); //선택된셀의 Text를 가져온다



36. UPDATE_COMMAND 사용하기

        pCmdUI->Enable(TRUE); //버튼 활성화

        pCmdUI->SetText((bAdd)?"취소":"신규"); //버튼의 text 설정

        pCmdUI->SetCheck(TRUE); //버튼 체크



37. 프로그램정보저장

  CWinApp::GetProfileString(섹션명,항목명,기본값); // 함수를 사용한다. (문자열)

  CWinApp::GetProfileInt(섹션명,항목명,기본값);  //불러올때사용 (숫자)

  CWinApp::WriteProfileString(섹션명,항목명,값); //저장할때 사용 (문자열)

  CWinApp::WriteProfileInt(섹션명,항목명,값); //저장할때 사용 (숫자)

  //불러올때 사용할함수

  void CMainFrame::ActivateFrame(int nCmdShow)  //프로그램 실행후 프레임생성될때 실행

  //저장할 때 WM_DESTROY 메시지 사용



38. 컨트롤바 표시하기

        CMainFrame *pMain=(CMainFrame *)GetParent(); //MainFrame 주소가져오기

        pMain->ShowControlBar(&pMain->m_wndToolBar,bTool1,FALSE); //툴바를 bTool2 에따라 보이고 감춘다



39. Window 창크기,위치정보 저장하기

MainFrame 의 WM_DESTROY 에

        WINDOWPLACEMENT w;

        this->GetWindowPlacement(&w); //윈도우의 정보를 저장한다.

        CString strRect;

        strRect.Format("%04d,%04d,%04d,%04d", //04d 는 4자리 확보하고 남은건 0으로 채워라

                w.rcNormalPosition.left,w.rcNormalPosition.top,

                w.rcNormalPosition.right,w.rcNormalPosition.bottom); //윈도우의 위치,크기 확보..

       

        BOOL bMax,bMin; //윈도우의 상태를 저장하기위한 변수

        //w.falg 는 이전상태의 정보를 가지고 잇다!!

        if(w.showCmd==SW_SHOWMINIMIZED)           //최소화 상태

        {

                bMin=TRUE;

                if(w.flags==0) //falg 값이 0 이면 이전 상태가 보통상태이다!!

                        bMax=FALSE;

                else    //이전상태가 최대화 상태

                        bMax=TRUE;

        }

        else                            

        {

                if(w.showCmd==SW_SHOWMAXIMIZED) //최대화상태

                {

                        bMax=TRUE;

                        bMin=FALSE;

                }

                else  //보통 상태

                {

                        bMax=FALSE;

                        bMin=FALSE;

                }

        }

        AfxGetApp()->WriteProfileString("WinStatus","Rect",strRect);

        AfxGetApp()->WriteProfileInt("WinStatus","Max",bMax);

        AfxGetApp()->WriteProfileInt("WinStatus","Min",bMin);



//읽어올차례..

ActivateFrame 함수로 가서

        WINDOWPLACEMENT w;  //윈도우의 상태를 저장하는 구조체..

        BOOL bMax,bMin;               //최대,최소상태를 저장할 변수

        CString strRect; //창크기를 받아올 변수

        strRect=AfxGetApp()->GetProfileString("WinStatus","Rect","0000,0000,0500,0700");

        bMin=AfxGetApp()->GetProfileInt("WinStatus","Min",FALSE);

        bMax=AfxGetApp()->GetProfileInt("WinStatus","Max",FALSE);

        int a=atoi(strRect.Left(4)); //문자열을 int 로 바꿔준다.

        int b=atoi(strRect.Mid(5,4));     //atoi 아스키 값을 int형으로 바꿔준다..

        int c=atoi(strRect.Mid(10,4));

        int d=atoi(strRect.Mid(15,4));

        w.rcNormalPosition=CRect(a,b,c,d);

        if(bMin)

        {

                w.showCmd=SW_SHOWMINIMIZED;

                if(bMax)

                {

                        w.flags=WPF_RESTORETOMAXIMIZED  ;

                }

                else

                {

                        w.flags=0;

                }

        }

        else

        {

                if(bMax)

                {

                        w.showCmd=SW_SHOWMAXIMIZED;

                }

                else

                {

                        w.showCmd=SW_SHOWNORMAL;

                }

        }

        this->SetWindowPlacement(&w); //설정된 값으로 윈도우를 그리게 한다..

       

        //CFrameWnd::ActivateFrame(nCmdShow); //이건 반드시 주석처리한다..



40. progress Bar 쓰기



        m_progress.SetRange(m_first,m_last); //Progress 범위설정하기

        m_progress.SetStep(m_step); //Progress Step설정하기

        //m_progress.StepIt(); //스텝만큼 움직이기

        //또는 다음을 사용한다

        for(int a=m_first;a<=m_last;a+=m_step) //a가 처음부터 끝까지

        {

                m_progress.SetPos(a); // 위치를 a에 맞춘다

                Sleep(50); //천천히 움직이게한다

        }



41. 파일대화상자 FileDialog 사용하기

void CConDlg1::OnFileopen()  //파일열기 버튼

{

        CFileDialog *fdlg; //파일대화상자 객체 생성 // 포인터로 만든다..

        static char BASED_CODE szFilter[] = "Animate Video Files (*.avi)|*.avi|All Files (*.*)|*.*||";

        //필터를 만들어 준다..이건 할줄 모름..

        fdlg =new CFileDialog(TRUE, ".avi", NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,szFilter);

        //대화상자 만들기..이렇게 해야댄다..

        if(fdlg->DoModal()==IDOK) //이제..대화상자를 띠우고..    

        {                               //OK 누르면 실행될 부분..

                m_filename=fdlg->GetPathName();        //대화상자에서 경로를 받아서 저장.

                UpdateData(FALSE);    

        }

}

선생님이 해준거 //파일 다이얼로그 만들기

CFileDialog fdlg(TRUE,"avi",".avi",OFN_OEVRWRITEPROMPT,"Vidoe Files(*.avi)|*.avi|All Files(*.*)|*.*||");



42. Animate Control 사용하기

        m_animate.Open(m_filename); //파일을 연다

        m_animate.Play(0,-1,1);  //(처음프레임,마지막프레임,반복횟수)

        m_animate.Stop(); //정지시키기

        m_ani.SetAutoStart(TRUE); //자동으로 시작한다

43. Control 의 Style 바꿔주기

        Control.ModyfyStyle(제거할스타일,추가할스타일); //스타일은 MSDN내용 참조



44. 시스템 날자바꾸기 버튼

//SetSystemTime(),GetSystemTime() //GMT 표준시를 가져온다.

//GetLocalTime(),SetLocalTime()  //현재 지역시간을 가져온다.



        SYSTEMTIME st;

        GetLocalTime(&st); //현재 시간, 날자를 넣는다.

        st.wYear=m_date2.GetYear();

        st.wMonth=m_date2.GetMonth();

        st.wDay=m_date2.GetDay();

        SetSystemTime(&st);



45. 시스템 시간 바꾸기 버튼

        UpdateData(TRUE);

        SYSTEMTIME st;

        GetLocalTime(&st);

        st.wHour=m_time.GetHour();

        st.wMinute=m_time.GetMinute();

        st.wSecond=m_time.GetSecond();

        SetLocalTime(&st);



46.시스템의 드라이브 문자 얻기



        char temp[50];

        GetLogicalDriveStrings(sizeof(temp),temp);

        CString str,str1;

        int n=0;

        while(*(temp+n)!=NULL)

        {

                str=temp+n;

                str1+= " "+str.Left(2);

                n+=4;

        }



47. 현재 작업경로 얻기

        char temp[MAX_PATH]; //MAX_PATH 는 경로길이의 최대를 define 해놓은것.

        GetCurrentDirectory(sizeof(temp),temp);  // 현작업하는 경로를 얻어온다.(경로 길이,문자형);



48. Tree Control 사용하기

        HTREEITEM hmov,hmus; //핸들을받을 변수 이게 잇어야 하위 디렉토리 생성가능

        hmov=m_tree.InsertItem("영화",TVI_ROOT,TVI_LAST); //,TVI_ROOT,TVI_LAST는 default

        hm1=m_tree.InsertItem("외화",hmov);  //hmov 아래 “외화”트리 생성

        CImageList m_image; //그림을 사용하기 위한 클래스다!! 알아두자..

        m_tree.SetImageList(&m_image,TVSIL_NORMAL); //Tree View Style Image List => TVSIL

        hmov=m_tree.InsertItem("영화",0,1,TVI_ROOT,TVI_LAST); //,TVI_ROOT,TVI_LAST는 default

        hmus=m_tree.InsertItem("가요",1,2); //("문자열",처음그림번호,선택시그림)

        hm1=m_tree.InsertItem("외화",2,3,hmov); //그림 번호는 default 로 0이 들어간다..



49. List Control 사용하기

        m_list.ModifyStyle(LVS_TYPEMASK, LVS_ICON); //리스트를 큰아이콘형태로 보인다

        m_list.ModifyStyle(LVS_TYPEMASK, LVS_SMALLICON);  //리스트를 작은아이콘형태로 보인다

        m_list.ModifyStyle(LVS_TYPEMASK, LVS_LIST); //리스트를 리스트형태로 보인다

        m_list.ModifyStyle(LVS_TYPEMASK, LVS_REPORT); //리스트를 자세히형태로 보인다



        CImageList m_treeimage; //이미지리스트

        CImageList m_small, m_large;

        m_large.Create(IDB_LARGE,32,0,RGB(255,255,255)); //이거는 클래스에서 추가해준거다

        m_small.Create(IDB_SMALL,16,0,RGB(255,255,255)); (bmp ID값,

        m_list.SetImageList(&m_large,LVSIL_NORMAL);

        m_list.SetImageList(&m_small,LVSIL_SMALL);

        CString name[]={"홍길동","진달래","한국남","개나리"};

        CString tel[]={"400-3759","304-7714","505-9058","700-9898"};

        CString born[]={"1980-1-1","1981-12-20","1980-05-15","1981-08-31"};

        CString sex[]={"남자","여자","남자","여자"};

       

        m_list.InsertColumn(0,"이름",LVCFMT_LEFT,70);

        m_list.InsertColumn(1,"전화번호",LVCFMT_LEFT,80);

        m_list.InsertColumn(2,"생일",LVCFMT_LEFT,90);

        m_list.InsertColumn(3,"성별",LVCFMT_LEFT,50);

        LVITEM it; //리스트 구조체

        char temp[100];

        for(int a=0;a<4;a++)

        {      

                int n=(sex[a]=="남자")?0:1;

                m_list.InsertItem(a,name[a],n); //insert item 은 행을 만들고..

                it.mask=LVIF_TEXT|LVIF_IMAGE; //마스크 설정

                it.iItem=a;

                it.iSubItem=1; //열 설정

                strcpy(temp,tel[a]); //이거 모하는거냐..

                it.pszText=temp;

                m_list.SetItem(&it);                      // setitem 열에 정보를 넣는다.



                it.iSubItem=2; //열 설정

                strcpy(temp,born[a]); //이거 모하는거냐..

                it.pszText=temp;

                m_list.SetItem(&it);                      // setitem 열에 정보를 넣는다.



                it.iSubItem=3; //열 설정

                strcpy(temp,sex[a]); //이거 모하는거냐..

                it.pszText=temp;

                m_list.SetItem(&it);                      // setitem 열에 정보를 넣는다.




50. Bitmap Button 사용하기

  CBitmapButton 을 사용한다! CButton 에서 상속 받는클래스임..

        m_button1.Create(NULL,

                WS_CHILD|WS_VISIBLE|BS_OWNERDRAW,CRect(310,20,370,50),

                this,IDC_MYBUTTON); //버튼만들기

        m_button1.LoadBitmaps(IDB_UP,IDB_DOWN,IDB_FOCUS,IDB_DISABLE); //버튼의 그림설정

        m_button1.SizeToContent(); //버튼을 그림 크기로 맞춰 준다!!



 그냥 버튼을 비트맵버튼으로 바꾸기 -> 버튼을 만든다 속성에서 OWNERDRA 속성에 체크!!

        m_button2.LoadBitmaps(IDB_UP,IDB_DOWN,IDB_FOCUS,IDB_DISABLE); //버튼의 그림설정

        m_button2.SizeToContent(); //버튼을 그림 크기로 맞춰 준다!!



51. 중복없는 난수발생하기

        int su; //발생된 난수저장

        int a,b;

        BOOL bDasi; //숫자가중복될경우 다시하기위한 변수

        for(a=0;a<9;a++)  //난수 9개 발생

        {

                bDasi=TRUE;

                while(bDasi)

                {

                        bDasi=FALSE;

                        su=rand()%10; //난수발생

                        for(b=0;b

                        {

                                if(temp[b]==su)  //중복이면

                                {

                                        bDasi=TRUE; //중복이 잇으면 다시while 문을 실행한다

                                        break;

                                }//if

                        }//for

                }//while

                temp[a]=su; //중복이 아니면 대입한다



52. 메뉴 범위로 사용하기

  ON_COMMAND_RANGE(ID_LEVEL3,ID_LEVEL9,OnLevel); //범위메세지 발생

  //메뉴 ID의 값이 연속된 숫자일 경우 범위로 지정해서 사용할수잇다



53. 한,영 전환함수

void CCustView::SetHangul(BOOL bCheck) //T:한글 F:영문 이건 외우자..

{

        HIMC hm=ImmGetContext(this->GetSafeHwnd()); //뷰클래스의 윈도우 핸들포인터를 얻는다.

        if(bCheck)

        {

                ::ImmSetConversionStatus(hm,1,0); //1은 한글 0은 영문

        }

        else

        {

                ::ImmSetConversionStatus(hm,0,0); //영문으로 바꿔준다

        }

        ::ImmReleaseContext(this->GetSafeHwnd(),hm); //장치를 풀어준다

}

#include "imm.h" //헤더 반드시 추가하고

imm32.lib (라이브러리 파일)를 반드시 링크해주어야 한다!

**** 라이브러리 추가하기

프로젝트메뉴 -> 셋팅 -> 링크탭



54. DLL함수정의하기

임포트함수 :  extern "C"  __declspec(dllimport)   리터형  함수명(매개변수,...) ;

  - 메인프로그램에서 DLL에 있는 함수를 호출할때 사용한다.



엑스포트함수 :  extern "C"  __declspec(dllexport)   리터형  함수명(매개변수,...)

                      {

                             내용;

                      } 

 
Posted by 나비:D
:

게임 개발 SDK

2009. 8. 26. 21:24
===== 관련 링크 ===== * DirectX SDK - (November 2008) - http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=5493f76a-6d37-478d-ba17-28b1cca4865a - 다운로드 센터 : http://www.microsoft.com/downloads/en/ 에서 "DirectX SDK"로 검색 - MSDN의 Downloads : http://msdn.microsoft.com/en-us/xna/aa937788.aspx * 도움말 - Reference : http://msdn.microsoft.com/en-us/library/bb174336(VS.85).aspx - Looting : http://telnet.or.kr/sec_directx/ * DirectX.DevPak for Dev-Cpp - http://www.dgrigoriadis.net/post/2004/06/26/DirectXDevPak-for-Dev-Cpp.aspx * NVIDIA - PerfHUD 6(퍼포먼스분석) : http://developer.nvidia.com/object/nvperfhud_home.html - Texture Viewer : http://developer.nvidia.com/object/windows_texture_viewer.html - DDS Thumbnail Viewer : http://developer.nvidia.com/object/dds_thumbnail_viewer.html - Photoshop Plugin : http://developer.nvidia.com/object/photoshop_dds_plugins.html#downloads * AMD(Shader Tool) - RenderMonkey : http://developer.amd.com/gpu/rendermonkey/Pages/default.aspx#download * Viewer - scarlet : http://shimtools.com/scarlet/download.htm - xnview : http://www.xnview.com/en/downloadwin32.html * DirectX DLL - http://blog.naver.com/masque 에서 카테고리:DirectX DLL - http://blackenginewiki.bl.ohost.de/BlackSnake-Studios/index.php?option=com_content&view=article&id=6&Itemid=8 * 구형그래픽카드 정보 - http://image.dcinside.com/download.php?id=programming&no=29bcc427b48577a16fb3dab004c86b6f25381b65ff9f405849651cf9e3717be52ce87e52459153e0b1eea5c5c0de1e2902a3bebe0fc00792bc66892c6c9385a5dc1d5a3eb356&f_no=0ebcc222c6d328b520a7daa6 * 가마수트라(번역) - http://www.gitiss.org/html/knowledge/list_policy.jsp?catid=0&page=1&insid=1487&compid=7&orderby=6&keyword= * 강좌 - dhpo ware : http://www.dhpoware.com/demos/index.html - Code Sampler : http://www.codesampler.com/dx9src.htm * API Call비용 : http://msdn.microsoft.com/en-us/library/bb172234(VS.85).aspx ===== DirectX SDK를 VS에 추가하기 ===== 1. "도구:옵션:프로젝트 및 솔루션:VC++ 디렉터리:다음 파일의 디렉터리 표시"에서 2. "포함 파일"을 선택 3. 목록중 빈공간 클릭 4. ... 클릭 5. DX가 설치된곳의 "Include"선택 예) "C:\Program Files (x86)\Microsoft DirectX SDK (November 2008)\Include" 6. "라이브러리 파일" 선택 7. 목록중 빈공간 클릭 8. ... 클릭 9. DX가 설치된곳의 "Lib\x86"선택 예) "C:\Program Files (x86)\Microsoft DirectX SDK (November 2008)\Lib\x86" 10. 확인
Posted by 나비:D
:

소프트웨어 오디오 코덱 low-level document

1. 개요

1.1 오디오 부분의 동작

본 화상회의 애플리케이션에서 오디오 부분의 절차는 다음과 같다.

1) 오디오 출력 디바이스(Audio Output Device)를 연다.
세션(Session)에 참가하기 전에 이미 세션에 참가중인 다른 사람들의 음성 데이터를 받아 이를 디코딩하기 위해 오디오 출력 디바이스(Audio Output Device)가 열려있어야 한다.

2) 오디오 입력 디바이스(Audio Input Device)를 연다.
자신이 마이크로폰으로 말하는 음성을 인코딩하기 위한 오디오 입력 디바이스(Audio Input Device)가 열려있어야 할 것이다.

3) 세션에 참가
세션에서 음성의 송신과 수신을 위해 할당된 멀티캐스트 주소와 포트로 데이터그램(datagram) 소켓을 바인드(Bind)해 놓는다.

4) 디코딩
1-3의 초기화 과정을 마치면 그 소켓을 통해 참가한 세션에서 현재 말하고 있는 사람의 음성 데이터를 받게 되고, 이를 오디오 출력 디바이스를 통해 디코딩하여 음성을 듣게 된다.

5) 인코딩
1-3의 초기화 과정을 마친후 세션 제어에 의해 자신에게 발언권이 주어지게 되면 이제 내가 마이크로폰으로 말하는 음성이 오디오 입력 디바이스를 통해 샘플링 되고 이 음성 데이터를 위에서 말한 음성을 위한 소켓을 통해 네트워크로 전송한다.

이러한 절차들에 대해선 2절 오디오 코덱 구현에서 자세히 다루도록 한다.

1.2 오디오 데이터 형식 :

본 애플리케이션에는 사용하는 오디오 데이터 형식으로 8-bit PCM 방식을 사용하며 이 형식의 특징은 다음과 같다.

1) 초당 8000번을 샘플링
2) 각 샘플을 8bit로 나타냄
3) 이 형식을 사용하는 데이터는 0에서 255(0xFF)사이의 값을 갖게 된다.
4) 64Kbps 이상의 네트워크 대역폭(Bandwidth)을 요구 (초당 8000번을 샘플링하고, 한 샘플을 한 byte로 나타내므로 초당 8kbyte의 오디오 데이터가 생성된다고 할 수 있다. 따라서 초당 8kbyte, 즉 64kbit를 전송해야 하므로 헤더 부분을 고려하 면 64Kbps 이상의 네트워크 대역폭(Bandwidth)을 사용한다고 할 수 있다.)

1.3 오디오 데이터 교환 방식

본 애플리케이션에는 세션의 참가자의 수에 따라 오디오 데이터의 교환 방식이 full-duplex 방식에서 half-duplex 방식으로, 또는 역으로 전환된다.

1) Full-duplex 방식 : 2명만이 세션에 있게 될 때는 서로의 음성을 full-duplex방식으로 서로 주고받을 수 있다.
2) Half-duplex 방식 : 세션에 3명 이상이 있게 될 때는 세션 제어에 의해 한 사람에게만 발언권이 주어지는 Half-duplex 방식으로 전환된다.

1.4 오디오 서비스를 제공하는 window 95의 멀티미디어 API

Windows 95는 본 화상회의 애플리케이션에서 다루어야 하는 파형 오디오(Waveform Audio)를 사용하기 위한 여러 다양한 기능들을 포함한 멀티미디어와 관련된 기능들을 멀티미디어 API로 제공한다. Windows 95의 멀티미디어 API에서는 오디오 서비스의 여러 가지 타입과 레벨들을 제공한다. 오디오 서비스의 다른 타입들은 각기 다른 포맷과 다른 기술을 필요로 하는데, 제공하는 오디오 서비스의 타입들에는 파형(Waveform) 오디오 서비스, MIDI 오디오 서비스, 컴팩트 디스크 오디오 서비스가 있다. 본 화상회의 애플리케이션에서 다루는 파형 오디오 서비스는 디지털 오디오 하드웨어를 위한 디코딩과 인코딩을 지원한다. 오디오 서비스의 레벨에는 하이-레벨과 로우-레벨이 있다. 하이-레벨 오디오 서비스는 프로그래밍하기가 편하다는 장점이 있고, 로우-레벨 오디오 서비스는 오디오 디바이스 드라이버를 직접 억세스 하므로 복잡한 프로그래밍을 요구하지만 오디오 디코딩과 인코딩에 대해 미세한 제어까지 할 수 있는 장점이 있다. 오디오 서비스를 제공하는 window 95의 멀티미디어 API를 이용하는 애플리케이션의 구조는 <그림 2-1>과 같다.

 

2. 오디오 코덱 구현

2.1 구현 환경과 요구 사항

Windows 95 상에서 Visual C++ 4.0과 MFC, 그리고 로우-레벨(Low Level) 오디오 API를 이용하여 구현하였으며 팬트엄 166 이상의 프로세서를 가진 PC에서만 테스트해보았다. 요구 사항으로는 8-bit PCM 샘플링과 full-duplex를 지원하는 사운드 카드를 요구한다.

2.2 Low-Level 오디오 API를 이용하여 오디오를 인코딩/디코딩하는 절차

로우-레벨(Low Level) 오디오 API는 애플리케이션으로 하여금 직접 오디오 디바이스 드라이버와 통하게 함으로써 로우-레벨 오디오 서비스를 제공한다. 로우-레벨(Low Level) 오디오 API를 이용하여 파형 오디오(Waveform Audio)를 인코딩/디코딩하기 위한 절차를 간단히 기술하면 다음과 같다.

1) 원하는 오디오 데이터 형식이 사용가능한지를 아닌지를 검사한다.
2) 사용이 가능하다면 원하는 오디오 입출력 디바이스(Audio Input/Output Device)를 연다.
3) 디바이스로부터 파형 오디오(Waveform Audio) 샘플 데이터를 넘겨주고 받기 위해서 하나 이상의 데이터 버퍼 블록을 할당(Allocation)한다. 할당한 데이터 버퍼 블록은 처리를 기다리는 디바이스 큐(queue)로 보내져야 한다.
4) 디바이스가 데이터 버퍼 블록을 처리하면, 블록은 다른 처리(디코딩, 또는 전송) 또는 재사용을 위해서 애플리케이션에게 리턴 된다.

2.3 구현 내용

가. 초기화

1) 원하는 오디오 데이터 형식이 사용가능한지를 테스트

본 애플리케이션에서 사용하는 오디오 데이터 형식인 8-bit PCM 방식이 audio device를 통해 사용가능한지를 waveInOpen, waveOutOpen에 다음과 같은 parameter들로 호출하여 리턴되는 결과 값으로 알아낸다. (waveInOpen, waveOutOpen 대한 자세한 설명은 Visual C++의 Help를 참조) 관련된 코드는 다음과 같다.

< decl.h >

#define BLKS_PER_READ 4
#define READ_AHEAD 8
#define BLKS_PER_WRITE 4
#define WRITE_AHEAD 4

// 8-bit PCM format을 나타내는 WAVEFORMATEX 구조체
static const WAVEFORMATEX lin8fmt = {

    WAVE_FORMAT_PCM,
    1, // 1 is mono, 2 is streo
    8000,
    8000,
    1,
    8,
    0
};

< audioview.cpp의 audioview() 함수안에 있는 부분 >

int sts;

// 먼저 input audio device에 대해 알아본다.
sts = waveInOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0, WAVE_FORMAT_QUERY);

if ( sts ) // 지원되지 않음을 알린다.

    AfxMessageBox("not support 8bit 8kHz PCM audio input");
else { // 지원되면 audio input device에서 사용되어질 버퍼를 new를 써 // 서 Heap memory에 할당한다.
    iformat_ = &lin8fmt; // input format을 8-bit PCM으로 지정
    iblen_ = len;
    len *= READ_AHEAD;
    ibufStart_ = new u_char[len]; // input buffer start를 지정
    ibufEnd_ = ibufStart_ + len; // input buffer end를 지정
    tmp = new u_char[len];
}

// 다음 output audio device에 대해 알아본다.
sts = waveOutOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0, WAVE_FORMAT_QUERY);

if ( sts ) // 지원되지 않음을 알린다.

    AfxMessageBox("not support 8bit 8kHz PCM audio output");
else { // 지원되면 audio output device에서 사용되어질 버퍼를 new를 써 // 서 Heap memory에 할당한다.
    oformat_ = &lin8fmt; // output format을 8-bit PCM으로 지정
    len = blksize * BLKS_PER_WRITE;
    oblen_ = len;
    len *= WRITE_AHEAD;
    obufStart_ = new u_char[len]; // output buffer start를 지정
    obufEnd_ = obufStart_ + len; // output buffer end를 지정
}

2) 파형 오디오 입출력 디바이스 열기

오디오 디바이스를 여는 함수(waveInOpen, waveOutOpen)들은 디바이스 식별자, 메모리 위치에 대한 포인터, 각 디바이스 타입에 독특한 파라미터들을 지정한다. 지정된 메모리 위치에는 디바이스 핸들 값이 들어가게 된다. 다른 로우-레벨 오디오 API들(waveInStart, waveInStop, waveOutWrite 등)을 호출할 때 열려진 오디오 디바이스를 식별하기 위해서 이 디바이스 핸들을 사용하여야 한다. ( 자세한 설명은 Visual C++의 Help를 참조)

디코딩을 위해서는 waveOutOpen함수를 사용하여 파형 오디오 출력 디바이스를 열고, 인코딩을 위해서는 waveInOpen함수를 사용하여 파형 오디오 입력 디바이스를 연다. 이 함수들은 파라미터로 주어지는 디바이스 식별자와 관계된 디바이스를 열고 그 디바이스에 대한 핸들을 파라미터로 지정된 메모리 위치에 써놓음으로써 리턴 한다.

관련된 코드는 다음과 같다. 파형 오디오 입력 디바이스를 여는 부분은 CAudioView::OpenIn() 함수에 있으며 파형 오디오 출력 디바이스를 여는 부분은 CAudioView::OpenOut() 함수에 있다.

// audio input device를 8-bit PCM format으로 open
void CAudioView::OpenIn()
{

    int error; // waveInOpen()의 return 값을 저장

    if (in_ == 0) // in_ : decl.h안에 HWAVEIN in_로 선언되어 있다.

      error = waveInOpen(&in_, WAVE_MAPPER, iformat_, (DWORD)(VOID*)waveInProc, (long)GetSafeHwnd(), CALLBACK_FUNCTION);
    // error 가 났으면 어떤 error인지를 알아낸다.
    if ( error == MMSYSERR_ALLOCATED)
      AfxMessageBox("Specified resource is already allocated.");
    else if ( error == MMSYSERR_BADDEVICEID)
      AfxMessageBox("Specified device identifier is out of range.");
    else if ( error == MMSYSERR_NODRIVER)
      AfxMessageBox("No device driver is present.");
    else if ( error == MMSYSERR_NOMEM)
      AfxMessageBox("Unable to allocate or lock memory.");
    else if ( error == WAVERR_BADFORMAT)
      AfxMessageBox("Attempted to open with an unsupported waveform-audio format.");
    ........................................
    // 오디오 입력 장치를 여는데 성공한 후에는 오디오 입력 장치를 통해 인코딩 될
    // 데이터가 저장될 버퍼를 ( 지원한지를 검사한 후 위에서 미리 할당된)
    // waveInPrepareHeader를 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있
    // 게끔 준비하고 waveInAddBuffer를 이용해 버퍼를 디바이스 드라이버에 보낸다.
    // waveInStart로 인코딩을 시작하기 전에는 반드시 위와 같은 방식대로 드라이버에
    // 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게 된다.
    // 위의 일을 하는 코드가 이 부분에 위치하며 이는 뒤의 파형 오디오 데이터 인코
    // 딩 부분에서 설명된다.
    ........................................
    return;
}

// audio output device를 8-bit PCM format으로 open
void CAudioView::OpenOut()
{

    int error = 0; // waveOutOpen()의 return 값을 저장

    if (out_ == 0) // in_ : decl.h안에 HWAVEIN in_로 선언되어 있다.

      error = waveOutOpen(&out_, WAVE_MAPPER, iformat_,(DWORD)(VOID*)waveInProc, (long)GetSafeHwnd(),CALLBACK_FUNCTION);
    ........................................

    // waveOutWrite함수에 보낼 파형 오디오 데이터 블록을 waveOutPrepareHeader함
    // 수를 사용해 준비한다. 이러한 일과 관계된 코드가 이 부분에 위치하며 이는 뒤
    // 의 파형 오디오 데이터 디코딩 부분에서 설명된다.
    ........................................
    return ;

}

3) 파형 오디오 입출력 데이터 타입

파형 오디오 입출력 함수들을 위해서 Windows 95의 멀티미디어 API에서 정의한 파형 오디오 데이터 타입들 중에서 본 애플리케이션에서 이용한 것들은 <표 2-1>과 같다. (자세한 사항은 VIsual C++의 Help를 참고)

<표 2- 1> 본 애플리케이션 구현에서 사용한 파형 오디오 데이터 타입
타입
설명
HWAVEOUT
열려진 파형 오디오 출력 디바이스에 대한 핸들
HWAVEIN
열려진 파형 오디오 입력 디바이스에 대한 핸들
WAVEHDR
파형 오디오 입력 또는 출력 데이터 블록에 대한 헤더로 쓰이는 구조체
WAVEFORMATEX
특정한 파형 오디오 입력 또는 출력 장치에 의해 지원되는 데이터 포맷을 명시하는 구조체
 

본 애플리케이션의 구현에서 사용되는 파형 오디오 데이터 타입들로 선언된 변수들의 정의에 관련된 코드는 다음과 같으며 decl.h에 정의되어 있다.

< decl.h >
const WAVEFORMATEX* iformat_; // input audio format
const WAVEFORMATEX* oformat_; // output audio format
HWAVEOUT out_; // 오디오 출력 장치에 대한 핸들
HWAVEIN in_; // 오디오 입력 장치에 대한 핸들
WAVEHDR iwhdr_[READ_AHEAD];
// 오디오 입력 데이터 블록에 대한 헤더에 대한 정의
WAVEHDR owhdr_[BLKS_PER_WRITE * WRITE_AHEAD];
// 오디오 출력 데이터 블록에 대한 헤더에 대한 정의

4) 파형 오디오 데이터 포맷 지정

waveOutOpen 또는 waveInOpen을 사용해 오디오 입출력 디바이스를 열 때 사용할 파형 오디오 데이터 포맷을 담은 WAVEFORMATEX 구조체에 대한 포인터를 파라미터로 전달한다. WAVEFORMATEX 구조체의 정의는 <표 2-2>와 같다. (자세한 사항은 VIsual C++의 Help를 참고)

<표 2- 2> WAVEFORMATEX 구조체
멤버 변수
설명
wFormatTag
파형 오디오 형식. PCM이면 WAVE_FORMAT_PCM
nChannels
파형 오디오 데이터의 채널의 개수. 1은 모노, 2는 스테레오를 의미한다.
nSamplesPerSec
샘플링율(Sampling rate). 파형 오디오 형식이 PCM이면 이 값은 8.0kHz, 11.025kHz, 22.05kHz, 44.1kHz 중의 하나여야 한다
nAvgBytesPerSec
요구되는 average data-transfer rate. 파형 오디오 형식이 PCM이면 이 값은 샘플링율과 block alignment의 곱과 같아야 한다.
nBlockAlign
block alignment. 파형 오디오 형식이 PCM이면 채널의 개수와 샘플당 비트 수를 8(bits per byte)로 나눈 값의 곱과 같아야 한다.
wBitsPerSample
Bits per sample. 파형 오디오 형식이 PCM이면 이 값은 8(모노 일 때) 또는 16(스테레오 일 때)이어야 한다.
cbSize
WAVEFORMATEX 구조체의 끝에 덧붙여질 추가 형식 정보의 크기
본 화상회의 애플리케이션에서는 음성의 샘플링(Sampling) 방식으로 PCM을 사용하였고, 초당 8000번을 샘플링하고, 각 샘플을 8비트로 나타내는 8-bit PCM 방식의 파형 오디오 데이터 형식을 사용하므로 본 애플리케이션에서 WAVEFORMATEX 구조체의 멤버 변수의 값은 <표 2-3>과 같다.
<표 2- 3> 본 애플리케이션에서 WAVEFORMATEX 구조체의 멤버 변수의 값
멤버 변수
사용한 값
wFormatTag
WAVE_FORMAT_PCM
nChannels
1(모노)
SamplesPerSec
8000(8kHz의 샘플링)
nAvgBytesPerSec
8000
nBlockAlign
1
wBitsPerSample
8(한 샘플을 8bit로 표현)
cbSize
0(추가 정보 없음)
 

나. 파형 오디오 데이터 인코딩과 디코딩

1) 파형 오디오 데이터 디코딩

파형 오디오 출력 디바이스를 열었으면 이제 waveOutWrite함수를 사용하여 음성 데이터 블록을 오디오 출력 디바이스로 보내면 오디오 출력 디바이스는 이를 디코딩한다. waveOutWrite함수에 보낼 파형 오디오 데이터 블록을 지정하기 위해서는 WAVEHDR 구조체를 사용한다. 이 구조체는 데이터 블록에 대한 포인터와 데이터 블록의 크기와 몇 가지의 플래그를 포함한다. ( 자세한 설명은 Visual C++의 Help를 참조) 이 데이터 블록은 사용 전에 파형 오디오 출력 디바이스가 사용 할 수 있게끔 waveOutPrepareHeader함수를 사용해 준비되어져야 한다.

waveOutPrepareHeader함수를 사용하여 미리 할당해 둔 데이터 블록을 오디오 출력 장치가 사용할 수 있도록 준비하는 코드는 아래와 같으며 이 부분은 CAudioView::OpenOut() 함수 안에 위치하며 오디오 출력 장치를 성공적으로 연 경우에만 수행된다.

// 파형 오디오 출력 디바이스를 열었으면 이제 waveOutWrite함수를 사용하여
// 음성 데이터 블록을 오디오 출력 디바이스로 보내면 오디오 출력 디바이스는
// 이를 디코딩한다. waveOutWrite함수에 보낼 파형 오디오 데이터
// 블록을 지정하기 위해서는 WAVEHDR 구조체를 사용한다. 이 구조체는
// 데이터 블록에 대한 포인터와 데이터 블록의 크기와 몇 가지의 플래그를 포
// 함한다. 이 데이터 블록은 사용 전에 파형 오디오 출력 디바이스가 사용 할
// 수 있게끔 waveOutPrepareHeader함수를 사용해 준비되어져야 한다.

if ( error == MMSYSERR_NOERROR ) {

    // waveOutOpen success
    /* (re-)initialize the output buffer descriptors */
    memset(owhdr_, 0, sizeof(owhdr_));
    u_char* bp = obufStart_;
    obuf_ = bp;
    u_int len = oblen_;
    int i;

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

      WAVEHDR* whp = &owhdr_[(i + 1) * BLKS_PER_WRITE - 1];
      whp->dwFlags = 0;
      whp->dwBufferLength = oblen_;
      whp->lpData = (char*)bp;
      waveOutPrepareHeader(out_, whp, sizeof(*whp));
      whp->dwFlags |= WHDR_DONE;
      bp += len;
    }
}

데이터 블록을 waveOutWrite함수를 사용해 오디오 출력 디바이스에 보낸 후, 디바이스 드라이버가 그 데이터 블록에 대한 처리를 끝내고 나면 이 데이터 블록을 해제시킬 수 있다. WAVEHDR 구조의 멤버 변수인 lpData는 파형 오디오 데이터 샘플들에 대한 포인터이다.

본 애플리케이션의 구현에서는 오디오 데이터 블록의 디코딩은 세션에 참가 할 때 음성을 위해 바인드해놓은 소켓으로 데이터를 받을 때마다 수행한다. 더 자세한 설명은 뒤의 오디오 데이터의 전송 부분에서 다룬다. 디코딩을 마친 데이터 블록을 특별히 해제시키지 않고 이후의 받은 데이터에 의해 덮어 쓰여지도록 하였다. 좀 더 자세한 설명은 파형 오디오 데이터의 수신에서 언급한다. 오디오 데이터 블록의 디코딩에 해당하는 부분은 소켓으로 데이터를 받을 때마다 호출되는 ProcessPendingRead()에 있으며 다음과 같다.

void CAudioView::ProcessPendingRead()
{
WAVEHDR* cp = &owhdr_[(i + 1) * BLKS_PER_WRITE - 1];
// 현재 네트웍으로부터 받은 오디오 데이터 블록을 waveOutWrite함수를 사용
// 하여 디코딩하기 위해서는 WAVEHDR 구조체를 사용하여 그 오디오 데이터
// 블록을 지정해서 waveOutWrite함수의 파라미터로 사용해야 하므로 여기서
// OpenIn()에서 waveOutPrepareHeader함수를 사용해 오디오 출력 장치가 사
// 용할 수 있도록 준비해 둔 WAVEHDR 구조체 타입의 변수 array인 owhdr중
// 의 하나를 지정한다.
 

    i = ( i + 1 ) % WRITE_AHEAD ;
    // 다음 오디오 데어터 블록을 위해서는 owhdr array의 index를 하나 증가한다.

    int nBytes = m_pSocket->ReceiveFrom(Revpkt, PDU_SIZE, (CString&)m_strRevIP, (UINT&) m_Revport);

    if ( nBytes == -1 ) {
    // error를 처리하는 루틴
    }

    // 현재 받은 packet의 id 필드가 현재 발언권을 얻은 사람의 id와 같은지를
    // 검사한다. 또한 자신의 id와 같은지를 검사한다. 발언권을 얻지 않은 사람의
    // packet이거나 자신이 보낸 packet의 경우에는 디코딩을 하지 않는다.

    if ( (strcmp(Revpkt->id , m_CurrentSayingID ) != 0) || ( strcmp(Revpkt->id , m_ID) == 0) )
    return;

    // packet의 구조체의 정의는 decl.h에 다음과 같이 정의되어 있다.
    // typedef struct {
    // char id[9];
    // u_char data[8192];
    // } Packet;

    // 그러므로 순수한 오디오 데이터의 size는 packet size에서 id의 size 9를
    // 뺀 값이므로 이 값을 오디어 데이터 블록을 지정하는 WAVEHDR의 변수인
    // cp의 dwBufferLength에 넣는다.
    cp->dwBufferLength = nBytes - 9;

    // packet의 data를 오디어 데이터 블록을 지정하는 WAVEHDR의 변수인 cp의
    // lpData에 copy한다.
    memcpy(cp->lpData ,Revpkt->data ,cp->dwBufferLength);
    // ---> data copying problem : protocol overhead

    cp->dwFlags = WHDR_DONE ;
    // 네트웍으로부터 받은 packet의 오디오 데이터를 위와 같이 WAVEHDR의 변
    // 수인 cp로 변환한 후 waveOutWrite를 사용해 디코딩을 한다.

    waveOutWrite(out_, cp, sizeof(*cp));

}

2) 파형 오디오 디코딩을 관리하기 위한 Windows Message의 사용

파형 오디오 디코딩을 관리하도록 window procedure 함수 또는 애플리케이션에 의해 제공되는 파형 오디오 출력 장치에 대한 콜백 함수에는 <표 2-4>에서와 같은 메시지들이 올 수 있다. (괄호 안은 콜백 함수에게 전달되는 메시지임.)

<표 2- 4> 파형 오디오 디코딩 관리를 위한 Window Message들
message
설명
MM_WOM_CLOSE
(WOM_CLOSE)
waveOutClose함수를 사용하여 디바이스를 닫았을 때
MM_WOM_DONW
(WOM_DONE)
디바이스 드라이버가 waveOutWrite함수를 사용하여 보내진 데이터 블록에 대한 처리를 마쳤을 때
MM_WOM_OPEN
(WOM_OPEN)
waveOutOpen함수를 사용하여 디바이스를 열었을 때
이 메시지들에 대한 wParam은 항상 열려진 파형 오디오 디바이스에 대한 핸들 값이다. MM_WOM_DONE(WOM_DONE)의 경우에는 이 메시지의 파라미터로 전달받는 lParam의 값은 디코딩이 끝난 데이터 블록에 대한 WAVEHDR 구조체에 대한 포인터 값이다. 이를 가지고 디코딩이 끝난 데이터 블록을 해제(free)시킬 수 있다.

본 구현에서는 파형 오디오의 디코딩 관리를 특별히 하지 않고 디코딩이 끝난 데이터 블록은 이후의 소켓을 통해 받은 데이터로 overwrite되도록 하였다.

3) 파형 오디오 인코딩

파형 오디오 입력 디바이스를 연후에는 파형 오디오 데이터를 인코딩 할 수 있다. 파형 오디오 데이터는 애플리케이션에서 WAVEHDR 구조체로 지정해 놓은 버퍼에 인코딩 되어진다. 이 데이터 블록들은 미리 waveInPrepareHeader를 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있게끔 준비되어져 있어야 한다.

멀티미디어 API에서 파형 오디오 녹음(waveform-audio recording)을 처리하기 위해 제공하는 함수들은 <표 2-5>와 같다.

<표 2- 5> 파형 오디오 인코딩을 처리하기 위해 제공하는 함수들
함수
설명
waveInAddBuffer
디바이스 드라이버에 버퍼를 보낸다. 그러면 이 버퍼에는 디바이스 드라이버에 의해 인코딩된 파형 오디오 데이터가 채워질 수 있게 된다.
waveInStart
파형 오디오 인코딩을 시작한다.
waveInStop
파형 오디오 인코딩을 멈춘다.
waveInAddBuffer 를 통해 디바이스 드라이버에 보내진 버퍼가 인코딩된 파형 오디오 데이터로 채워지면 애플리케이션은 콜백 메시지를 받아서 이를 알게 된다. waveInStart로 인코딩을 시작하기 전에는 반드시 드라이버에 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게 된다.

waveInPrepareHeader함수를 사용하여 미리 할당해 둔 데이터 블록을 오디오 출력 장치가 사용할 수 있도록 준비하는 코드와 waveInAddBuffer를 사용해 버퍼를 디바이스 드라이버에 보내는 코드는 아래와 같으며 이 부분은 CAudioView::OpenIn() 함수 안에 위치하며 오디오 입력 장치를 성공적으로 연 경우에만 수행된다.

// waveform audio data는 애플리케이션에서 WAVEHDR 구조체로 지정해 놓은
// 버퍼에 인코딩 되어진다. 이 데이터 블록들은 미리 waveInPrepareHeader를
// 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있게끔 준비되어져 있어
// 야 한다. 다음은 waveInAddBuffer를 이용해 버퍼를 디바이스 드라이버에 버
// 낸다. waveInStart로 인코딩을 시작하기 전에는 반드시 위와 같은 방식대로
// 드라이버에 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게 된다.

/* (re-)initialize the input buffer descriptors */
memset(iwhdr_, 0, sizeof(iwhdr_));
// iwhdr_ : decl.h안에 WAVEHDR iwhdr_[READ_AHEAD]로 선언된어 있다.
ibindx_ = 0;
rbuf_ = rbufEnd_;
u_char* bp = ibufStart_;
u_int len = iblen_;
memset(bp, 0, len * READ_AHEAD);

// 사용되는 버퍼를 모두 초기화한다.
for (int i = 0; i < READ_AHEAD; ++i) {

    WAVEHDR* whp = &iwhdr_[i];
    whp->dwFlags = 0;
    whp->dwBufferLength = len;
    whp->lpData = (char*)bp;
    bp += len;
    waveInPrepareHeader(in_, whp, sizeof(*whp));
    if(MMSYSERR_NOERROR!=waveInAddBuffer(in_,whp,sizeof(*whp))) {
      AfxMessageBox("waveInAddBuffer error");
      return ;
    }
} // end of for loop

4) 파형 오디오 인코딩을 관리하기 위한 Windows Message의 사용

파형 오디오 인코딩을 관리하도록 window procedure 함수 또는 애플리케이션에 의해 제공되는 파형 오디오 출력 장치에 대한 콜백 함수에는 <표 2-4>에서와 같은 메시지들이 올 수 있다. (괄호 안은 콜백 함수에게 전달되는 메시지임. 자세한 사항은 Visual C++의 Help를 참조)

<표 2- 6> 파형 오디오 인코딩 관리를 위한 Window Message들
message
설명
MM_WIM_CLOSE
(WIM_CLOSE)
waveInClose함수를 사용하여 디바이스를 닫았을 때
MM_WIM_DATA
(WIM_DATA)
디바이스 드라이버가 waveInAddBuffer함수를 사용하여 보내어진 데이터 블록을 인코딩된 오디오 데이터로 채웠을 때
MM_WIM_OPEN
(WIM_OPEN)
waveInOpen함수를 사용하여 디바이스를 열었을 때
MM_WIM_DATA(WIM_DATA) 의 파라미터로 전달되는 lParam은 버퍼를 식별하는 WAVEHDR 구조체에 대한 포인터의 값을 가진다. 이 버퍼는 파형 오디오 데이터로 완전히 채워지지 않았을 수도 있다. 버퍼가 채워지기 전에 인코딩이 정지될 수도 있기 때문이다. 따라서 버퍼의 유용한 데이터의 양을 알아내기 위해 WAVEHDR 구조체의 멤버 변수인 dwBytesRecorded를 사용한다. lParam을 가지고 애플리케이션이 그 데이터 블록에 대한 사용(본 화상회의 애플리케이션의 경우 네트워크로 전송)을 끝마쳤을 때 그 데이터 블록을 해제할 수 있다.

본 애플리케이션의 구현에서 오디오 인코딩 관리를 위한 코드는 VOID CALLBACK waveInProc()에 있으며 다음과 같다.

VOID CALLBACK waveInProc(HWAVEIN hwi, UINT uMsg, long dwInstance, DWORD dwParam1, DWORD dwParam2 )
{

    switch (uMsg)
    {
      case WIM_DATA:
      // 디바이스 드라이버가 waveInAddBuffer함수를 사용하여 보내어진 데
      // 이터 블록을 인코딩된 오디오 데이터로 채웠을 때
      {
        // MM_WIM_DATA(WIM_DATA)의 파라미터로 전달되는 lParam은 버퍼
        // 를 식별하는 WAVEHDR 구조체에 대한 포인터의 값을 가진다.
        WAVEHDR *whp = (LPWAVEHDR)dwParam1;
        WAVEHDR* cp = &tmphdr_;

        // 이미 선언해 놓은 WAVEHDR 구조체에 대한 포인터인 tmphdr_로
        // 인코딩된 오디오 데이터 블록에 대한 WAVEHDR을 가리키는
        // dwParam1 ( WAVEHDR *whp = (LPWAVEHDR)dwParam1 ) 의 값
        // 들을 copy한다.
        cp->dwFlags = whp->dwFlags;
        cp->dwBufferLength = whp->dwBufferLength ;
        dwParam1 = (DWORD) cp;

        memcpy(cp->lpData,whp->lpData , cp->dwBufferLength );
        // copy가 끝난 WAVEHDR의 오디오 데이터 블록을 null로 만든다.
        memset(whp->lpData, 0, whp->dwBufferLength);
        // MY_WIM_DATA 메시지를 발생시킨다. MY_WIM_DATA 메시지를 처
        // 리하는 루틴에서는 역시 파라미터로 오는 WAVEHDR의 오디오 데이
        // 터 블록 부분을 네트웍으로 전송한다.

        if ( PostMessage ((HWND) dwInstance , MY_WIM_DATA ,0 , dwParam1 ) == FALSE)

          AfxMessageBox("PostMsg error");
        // 사용이 끝난 WAVEHDR을 다시 waveInPrepareHeader을 사용해 오
        // 디오 입력 장치가 사용할 수 있도록 준비한다.
        waveInPrepareHeader(hwi, whp, sizeof(*whp));

        // 다음 waveInAddBuffer를 사용해 디바이스 드라이버에 해당 버퍼를
        // 보낸다.
        if(MMSYSERR_NOERROR!=waveInAddBuffer(hwi,whp,sizeof(*whp))) {

          AfxMessageBox("waveInAddBuffer error");
          return ;
        }
        break;
      }
    } // end of switch
}

다. 파형 오디오 데이터의 전송과 수신

1) 파형 오디오 데이터 블록의 크기

본 화상회의 애플리케이션에서는 WAVEHDR의 멤버 변수인 lpData가 가리키는 데이터 블록의 크기를 8Kbyte로 하였다. 본 화상회의 애플리케이션에서는 음성의 인코딩 방식으로 PCM을 사용하므로 1초당 8000번을 샘플링하고, 한 샘플을 1byte로 나타낸다. 따라서 한 데이터 블록의 크기를 8Kbyte로 하였으므로 한 데이터 블록은 1초 동안의 음성 데이터를 나타낸다. 또한 한 데이터 블록의 크기를 8kbyte이므로 애플리케이션은 데이터 블록에 오디오 데이터를 채웠다는 MM_WIM_DATA(WIM_DATA) 메시지를 운영 체제로부터 1초에 한번씩 받게 된다.

본 애플리케이션의 구현에서는 MM_WIM_DATA(WIM_DATA) 메시지를 처리하는 루틴에서 음성 데이터 블록을 음성의 전송을 위해 만들어둔 소켓을 통해 네트워크로 내보낸다. 애플리케이션은 1초마다 한번씩 MM_WIM_DATA(WIM_DATA) 메시지를 받을 것이고, 이때 8Kbyte의 음성 데이터를 전송하므로 1초의 네트워크 지연이 추가되어진다고 볼 수 있다. 물론 데이터 블록의 크기를 1Kbyte로 조정하면 추가되는 네트워크 지연을 1초에서 0.125초로 줄일 수 있지만, 테스트 해본 결과 이때는 너무 잦은 MM_WIM_DATA(WIM_DATA)의 처리에 대한 오버헤드와 받는 쪽에서도 음성 전송을 위한 소켓을 통해 데이터가 왔음을 알리는 window message의 잦은 처리에 대한 오버헤드로 시스템이 부하를 견디지 못하고 다운되는 현상이 종종 있었다. 이를 해결하고 시스템을 안정적으로 동작하게 만들기 위해서 데이터 블록의 크기를 8Kbyte로 정하여 사용하였다. 또한, 초당 8Kbyte, 즉 64kbit를 전송해야 하므로 헤더 부분을 고려하면 64Kbps 이상의 네트워크 대역폭을 사용한다고 할 수 있다

2) 파형 오디오 데이터의 전송

본 애플리케이션에서는 세션 제어에 의해서 자신이 발언권을 얻으면 CAudioView::TriggerPlay()를 호출함으로써 파형 오디오 데이터를 인코딩하기 시작한다. 발언권이 세션의 다른 참가자에게 주어지면 CAudioView::TriggerStop()를 호출함으로써 파형 오디오 데이터의 인코딩을 중단한다. 관련된 코드는 다음과 같다.

void CAudioView::TriggerPlay()
{

    // PlayFlag는 현재 인코딩 중이면 TRUE, 아니면 FALSE로 setting한다.
    if ( PlayFlag == FALSE ) {
      waveInStart(in_);
      PlayFlag = TRUE;
      .......
    }
}

void CAudioView::TriggerStop()
{

    if ( PlayFlag == TRUE ) {
      waveInStop(in_);
      PlayFlag = FALSE;
      .......
    }
}

4) 파형 오디오 인코딩을 관리하기 위한 Windows Message의 사용에서 설명한데로 한 데이터 블록에 대한 처리가 끝났음을 애플리케이션에게 알리는 MM_WIM_DATA(WIM_DATA)를 받으면 애플리케이션에서 MY_WIM_DATA라고 정의해 놓은 메시지를 포스트하고 다시 이 메시지를 애플리케이션이 받으면 MY_WIM_DATA를 처리하는 프로시저로 메시지 맵핑(Message Mapping) 해놓은 OnWimData가 수행된다. OnWimData에서는 파라미터로 전달받은 lParam이 가리키는 WAVEHDR 구조체의 멤버 변수인 lpData가 가리키는 파형 오디오 데이터 블록과 다른 오디오 스트림과 구별되기 위한 식별자를 포함한 패킷을 구성하고 이를 음성 전송과 수신을 위한 소켓을 통해 네트워크로 보낸다. <그림 2-2>

패킷 구조체의 정의는 다음과 같으며 decl.h에 선언되어 있다.

typedef struct {

    char id[9];
    u_char data[8192];//2048];
} Packet;

음성 데이터의 전송과 관련된 코드는 CAudioView::OnWimData() 함수안에 있으며 다음과 같다.

long CAudioView::OnWimData(UINT wParam , LONG lParam)
{

    // 파라미터로 전달되는 lParam은 버퍼를 식별하는
    // WAVEHDR 구조체에 대한 포인터의 값을 가진다.
    WAVEHDR *whp = (LPWAVEHDR)lParam;

    struct sockaddr_in my_addr;
    my_addr.sin_family = PF_INET;
    my_addr.sin_port = htons(m_port);
    my_addr.sin_addr.s_addr = inet_addr(m_strPeerIP);

    // packet에 자신의 id를 집어 넣는다.
    strncpy ( Sndpkt->id , m_ID , 9 );

    // packet의 data 필드에 오디오 데이터 블록을 copy한다.
    memcpy(Sndpkt->data , whp->lpData, whp->dwBufferLength);

    // 사용이 끝난 WAVEHDR 구조체의 lpData를 null로 만든다.
    memset(whp->lpData, 0, whp->dwBufferLength);

    // packet을 전송한다.
    m_pSocket->SendTo(Sndpkt, PDU_SIZE, (LPSOCKADDR)&my_addr, sizeof(my_addr));

    return 0L;

}

3) 파형 오디오 데이터의 수신

음성 전송과 수신을 위한 소켓을 통해서 데이터를 받으면 일단 패킷 중에서 식별자 필드를 보고 이 패킷이 세션 제어에 의해서 발언권을 얻은 사람의 음성 데이터인지 아닌지를 판단한다. 판단한 결과 발언권을 얻은 사람의 것이면 이 데이터를 이미 선언해 놓은 WAVEHDR의 lpData로 복사하고 WAVEHDR의 다른 멤버 변수들의 값을 적절히 세팅한 후, waveOutWrite의 파라미터로 이 WAVEHDR을 넘겨주고 호출하면 받은 음성 데이터가 디코딩 되게된다. <그림 2-2>

해당하는 부분의 코드는 소켓으로 데이터를 받을 때마다 호출되는 ProcessPendingRead()에 있으며 이는 1) 파형 오디오 데이터 디코딩 부분에 함께 나와 있다.

Posted by 나비:D
:
CString 에서 Char* (배열)

UDP 소켓 통신하면서

CString으로 받은 값을 Char* 로 센드 시켰을때

길이는 맞게 오는데 자꾸 한글자만 나와 ㅠ.ㅠ

구글링 네이뇬 검색 하면 아래 와 비슷한 내용들 많이 나오는데...
방법은
1. (LPSTR)(LPCTSTR)로 강제 형변환
2. CString str;
   str.GetBuffer(str.GetLength());
   해주시면 char *을 리턴합니다.
ps. 위 두가지 방법중에 2번을 추천합니다.
      그리고 GetBuffer를 사용하시면 ReleaseBuffer()를 사용해서 해제해 주셔야합니다.

CString msg = "abcdefg";
char* tempchar;

tempchar = LPSTR(LPCTSTR(msg));
머 요런식으로 해서

strlen(tempchar) === 1
msg.getLength === 6

머냐고요~~~

strcpy
memcpy 다 1글자씩 밖에 안드감 ㅠ.ㅠ


다시 구글링


char Buffer[255];
CString szString;
size_t CharactersConverted = 0;

wcstombs_s(&CharactersConverted, Buffer, szString.GetLength()+1, szString, _TRUNCATE);

위방법 ㅠ.ㅠ

3시간 정도 찾은거 같아 ㅠ.ㅠ

비록 땃짓도 많이 했지만 C#은 간단하구 좋았는데..

VS2005 VC++ 대략 알고리즘 짜는데 시간이 가는게 아니라... 타입케스팅 하다가 시간이 다 가버림 ㅠ.ㅠ 쥘쥘쥘쥘 꺼우져!!!!!!!
Posted by 나비:D
:

C++ char 형변환

2009. 4. 23. 17:02
[CString → char *]
CString str;
str = "Hello";
char* ss = LPSTR(LPCTSTR(str));

[CString → TCHAR]
 _tcscpy( szValue, (LPCTSTR)strValue);

[CString → int]

CString str;
int i = 0;
i = atoi((LPCTSTR)str);

[char * → CString]
char ss[] = "Hello";
CString str;
str.Format("%s", ss);

[char * → int]
char ss[10] = "Hello";
int i = _ttoi((LPCTSTR)ss);
Posted by 나비:D
:

이전에 만들었던 DLL을 실제로 사용하는 방법에 대해서 포스팅한다 ..


두 가지 방법이 있다 ..

먼저 .LIB파일을 사용하는 암시적인 방법과 .. 명시적인 방법이 있다.


암시적인 방법은 .LIB파일을 링크옵션에 추가시켜주어서 링크하는 것인데 이 방법을 사용하기

위해서는 당연히 .LIB파일이 필수적으로 존재해야 한다.



위 스크린샷 처럼 dll.LIB를 추가시켜주고 실제 dll에 구현되어 있는 함수를 가져다가 쓰면 된다.

무척이나 간단한 방법이다 ..


명시적인 방법에 대해서 알아보자.

명시적인 방법은 .LIB가 없는 경우에 사용되면 좋다.

하지만 그러기 위해서는 먼저 dll에 구현되어 있는 정확한 함수의 이름 ..

그리고 함수의 리턴 타입.. 아규먼트 형식과 갯수등을 매우 정확하게 알고 있어야 한다.


명시적인 방법에서 사용되는 주요 함수가 LoadLibrary와 GetProcAddress이다..

첫 번째 함수는 dll을 로드해서 인스턴스를 반환하고 GetProcAddress 는 LoadLibrary를 통해서

받은 dll 인스턴스를 통해서 인자로 넘겨주는 심볼의 함수 주소를 가져온다.

즉 실제 구현된 함수의 포인터를 가져오는 것이다.


간단한 예제를 통해 알아보자.



위 코드는 함수 포인터를 선언하는 내용이다.

위에서 잠깐 말했듯이 명시적인 DLL사용방법을 쓰기 위해서는 DLL 에 구현되어 있는 함수의

리턴 타입과 아규먼트 타입을 잘 알아야 한다고 하였다 ..

위에서 처럼 함수 포인터를 선언할 때 꼭 DLL에서 구현된 함수와 같은 타입으로 해주어야

실제 사용할 때 에러가 없다.



위 코드에서는 LoadLibrary를 사용하고 있다. 인자로 로드할 dll의 파일명을 넘겨준다.

그런데 반환되는 타입을 보니 HMODULE이다. 위에서는 인스턴스라고 했는데?


HMODULE과 HINSTANCE는 각각 16비트 시절에 코드역역과 데이터영역을 나누는데에서 의미가

있었지만 32비트로 넘어오면서 이 두개의 차이가 없어졌다.


아무튼 .. 반환된 값은 hDll로 넘겨지게 되고 if 문을 사용해서 로드가 되었는지 안되었는지 먼저

확인을 한다 ..



이제 GetProcAddress를 사용해서 포인터를 구해오고 그 포인터를 해당 함수형으로 타입캐스팅을

한 다음 실제 사용할 함수 포인터에 넣어준다.


이때 GetProcAddress의 두 번째 인자는 실제 DLL의 Export 정보에 등록된 이름이어야 한다.

만약 조금이라도 틀리면 포인터를 얻어올 수 없다,


이렇게 얻어온 함수 포인터는 if문으로 평가되어서 로드가 되었는지 안되었는지 확인한다.



두 가지 방법을 알아보았는데 두 가지 모두 장단점이 있는것 같다..

먼저 .LIB를 사용하는 암시적인 방법은 실제 프로그램을 개발하는 입장에서는 사용하기 편리하다.

그러나 만약 DLL이 없거나 DLL 안에 구현된 함수를 얻어오지 못하는 등의 예외상황에서

유연한 대처가 힘들다 ..


그러나 명시적인 사용방법에서는 그러한 예외상황에 대해서 유연하게 대처가 가능하다

위에서 본 코드들 처럼 if 문을 사용해서 예외처리가 가능하기 때문이다 ..


그리고 해커적인 입장에서는 명시적인 사용방법에 대해서 잘 인지하고 있다면 리버싱등을 할때

좋은 힌트가 될 수 있을것 같다.

Posted by 나비:D
:

BLOG main image
by 나비:D

공지사항

카테고리

분류 전체보기 (278)
Programming? (0)
---------------------------.. (0)
나비의삽질 (5)
Application (177)
C# (28)
JAVA (22)
ASP.NET (3)
C++ (19)
C (18)
.NET (0)
Visual Basic (12)
Flex (16)
Eclipse (8)
Delphi (16)
Visual Studio 2005 (5)
Embed (6)
Linux (2)
Mobile (1)
XML (1)
안드로이드 (2)
SQL (51)
Web (27)
etc. (14)
Omnia (0)
---------------------------.. (0)

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

달력

«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Total :
Today : Yesterday :