2009년 3월 31일 화요일

[VS 2008] Internet Explorer8 설치 후 Visual Studio 에서 발생하는 문제 우회 해결법

Internet Explorer8 설치 후 Visual Studio 에서 발생하는 문제 우회 해결법



 Internet Explorer8 설치 후 Visual Studio 마법사를 사용하고자 할 때 에러 메시지를 출력해 주고,
뒤 이어지는 대화상자 작업 역시 제대로 이루어 지지 않습니다.
즉, Visual Studio 마법사를 사용할 때
- 함수 추가
- 변수 추가
- 클래스 추가
- Smart Device 프로젝트 생성
- Smart Device 클래스 생성
...

조사한 바로는 Internet Explorer8 의  Custom security manager 처리에서 문제가 발생 한다고 하는 군요.


■ 에러 발생 시나리오
1. 다이얼 로그를 생성했습니다.



2. 다이얼 로그의 클래스 생성 위해 마우스 컨텍스트 메뉴 에서 "클래스 추가" 선택.



3. "웹 페이지 오류" 대화상자 등장!!



4. "MFC 클래스 마법사" 가 나타났지만  대화상자 ID 가 선택되어 지지 않으며
   제대로 동작 하지 않습니다.





■ 해결법Microsoft Visual C++ 팀이 블로그를 통해서 공개한 방법 이라고 합니다.

1. regedit 실행

2. “HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\InternetSettings\Zones”
    에서 “1000” 키 생성
    DWORD 엔트리 생성 (Name=1207, Type=REG_DWORD, Data=0x000000)






위와 같이 설정하면 제대로 동작 합니다.






cf ) http://blogs.msdn.com/eva/archive/2009/03/30/internet-explorer-8-visual-c.aspx

2009년 3월 30일 월요일

BOOL 사용시 주의 사항

BOOL 사용시 주의 사항


 Windows Data 타입은 WinDef.h 에 정의 되어 있다.
물론 MSDN 에서도 찾을 수 있다.
http://msdn.microsoft.com/en-us/library/aa383751.aspx


BOOL 의 정의는 다음과 같다.


즉 int 형 값을 가지게 된다.
따라서 일반적으로 bool 과 같은 목적으로 사용 한다면 문제를 일으킬 가능성이 있다.
bool 의 true = 1, false = 0 이 두가지 값을 명시적으로 정의 하고
BOOL 의 TRUE  = 1, FALSE = 0  이라고 정의 한다.

문제가 없어 보이지만 문제의 소지가 있는 것은 BOOL 형을 리턴값으로 사용하는 API 를 사용할 때 문제이다.

BackupEventLog 함수의 리턴 형은 BOOL 이다.
Return Value 값 설명을 보면 함수가 성공하면 0 이 아닌 값을, 실패 할 경우 0 을 리턴한다고 한다.
따라서 이 함수 동작이 성공 했다 하더라도 그 값을 TRUE(1) 과 비교 한다면 비교가 틀릴 수 있을 가능성이 생긴다.




또 한가지 예 로 GetMessge 함수도 주의해서 사용해야 한다.
원형은 아래와 같다.

BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);

사용 코드를 아래와 같이 작성 한다면

BOOL bRet;

while( (bRet = GetMessage( &msg, hWnd, 0, 0 )) != 0)

{

    if (bRet == -1)

    {

        // handle the error and possibly exit

    }

    else

    {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }

}

GetMessage 는 리턴값이 WM_QUIT 일때는 0, 에러 일때는 -1, 그 외의 값은 성공으로 판단한다.
즉, 성공하였을 때의 값은 항상 TRUE(1) 값은 아니라는 소리이다.



※ 참고
Visual C++ 에서 bool 이 가지는 의미
API의 BOOL 리턴값 비교시 주의 사항

2009년 3월 29일 일요일

::CreateThread, _beginthread, _beginthreadex, ::AfxBeginThread 차이점

::CreateThread,  _beginthread,  _beginthreadex,  ::AfxBeginThread 차이점


1. C/C++프로그래밍과 ::CreateThread  윈도우가 제공하는 CreateThread 함수는 스레드를 생성하는 함수이다. 하지만 C/C++ 로
코드를 작성하는 경우에는 CreateThread 를 사용해서는 안 되고, 마이크로소프트 C/C++
runtime-library 에서 제공하는 _beginthreadex 함수를 사용해야 한다. 다른 컴파일러
에서도 ::CreateThread 함수를 대체할 만한 함수를 제공할 것이며, 반드시 컴파일러에 의해
제공되는 다른 함수를 사용해야 한다.


2. 멀티 스레드 안전한 C/C++ Library  역사적으로 C runtime-library 개발자는 멀티 스레드 어플리케이션에서 C runtime-library 를
사용하였을 때 발생하는 문제에 대해서는 전혀 고려하지 않았다. 멀티 스레드 어플리케이션
에서 전통적인 C runtime-library 를 사용하였을 때 문제가 발생할 수 있다.
따라서 Microsoft 는 이러한  문제를 해결하기 위해서 스레드 안전한 C/C++ runtime-library 를
제공하고 있다.
멀티 스레드 안전한 C/C++ run-time library 함수는 다른 스레들로부터 영향을 받지 않도록
자신을 호출한 스레드의 데이터 블록에만 접근 가능하게 한다.


3. Single-thread C/C++Library 와 Multi-thread C/C++ LibrarySingle-thread C/C++Library 는 단일 스레드 전용의 함수들을 말하고,
Multi-thread C/C++ Library 는 멀티 스레드 전용의 함수들을 말한다.

   ※ Visual Studio 2008 에서는 Multi-thread C/C++ Library 만 지원한다.
       즉 더 이상 단일 스레드 전용의 C/C++ 라이브러리는 제공하지 않는다.





4. _beginthread 와 _endthread
  _beginthread 함수는 새로운 스레드를 생성하고 난 후 바로 ::CloseHandle 함수를 호출하여
새로 생성된 스레드의 핸들을 제거하게 된다. 따라서 _beginthread 함수 호출 이후에 이 스레드
핸들에 접근 할 수 없게 된다.
  _beginthread 함수의 이런 동작은 Win32의 상세함을 숨기기 위해 고안되었으나 결국 버그가
되어버린 함수이다. 따라서 마이크로소프트는 이러한 버그를 수정한 _beginthreadex 함수를 만들
게 되었다.
  _beginthread 함수는 ::CreateThread,  _begintheadex 함수에 비해 매개변수의 개수가 적다.
보안특성을 가진 스레드를 생성할 수 없으며, 일시 정지된 상태의 스레드도 생성할 수 없고,
스레드의 ID 값을 얻을 수도 없다.
  _beginthread 함수는 스레드가 종료되면 자동으로 _endthread 함수를 호출 하는데 이 함수는
어떠한 매개변수도 가지고 있지 않기 때문에 스레드의 종료 코드는 항상 0 이다.


5. _beginthreadex 와 _endthreadex  ::CreateThread 함수의 문제점을 보완하기 위해 C/C++ runtime-library 가 제공하는 스레드를
생성하는 함수로서 각 스레드 별로 적절한 구조의 데이터 블록을 생성해 준다.
::CreateThread 함수와 동일한 매개변수를 가지고 있지만 매개변수의 이름과 형태가 일치 하지
않는다.  C/C++ run-time library 가 윈도우의 자료형에 의존성을 가지지 않도록 개발했기 때문
이다.
  _beginthreadex 함수의 반환값은 ::CreateThread 함수의 반환값과 같은 새로 생성된 스레드의
핸들 값이다. 하지만 자료형이 정확하게 일치하지는 않기 때문에 적절하게 형변환을 해줘야 한다.
  _beginthreadex 함수는 _beginthread 함수와는 달리 내부적으로 새로 생성한 스레드의 핸들을
제거하지 않기 때문에 명시적으로 ::CloseHandle 함수를 호출해 주어야 한다.
  _beginthreadex 함수에 의해 생성된 스레드가 종료되면 _endthreadex 함수가 자동으로 호출.


6. ::AfxBeginThread   MFC 에서 스레드(Worker thread, User interface thread)를 생성하는 함수이다.
::AfxBeginThread 함수는 실제 스레드 생성을 수행하지 않는다. 스레드 생성을 위한 객체를
생성하고 스레드 생성을 위해 CWinThread::CreateThread 함수를 호출하는 것이 전부이다.
실제 스레드 생성은 CWinThread::CreateThread 함수 내부에 구현되어 있다.
CWinThread::CreateThread 함수는 내부적으로 _beginthreadex 함수를 호출하여 스레드를
생성한다.

스레드의 기본

스레드의 기본



■ 스레드의 구성
  1.  프로세스는 프로세스 커널 오브젝트, 가상 주소 공간(프로세스 메모리) 2개의 요소로 구성된다.
     스레드도 이와 비슷하게 스레드 커널 오브젝트, 스레드 스택 으로 구성 된다.
   - 스레드 커널 오브젝트(Thread Kernel Object) : 운영체제가 스레드를 다루기 위해 스레드에 대한 통계정보를
                                                                     저장하는 공간(구조체)
   - 스레드 스택 (Thread Stack) : 스레드가 코드를 수행할 때 함수의 매개변수와 지역변수를 저장


  2. - 프로세스는 스스로 수행될 수 없고 적어도 하나의 스레드를(Primary Thread) 를 가져야 한다.
     - 프로세스는 어떤 것도 수행할 수 없으며 단순히 생각하면 "스레드의 저장소" 라고 볼 수 있다.
     - 스레드는 항상 프로세스의 컨텍스트 내에서 생성되며 프로세스 안에만 살아 있을 수 있다.
    
    ※ 프로세스의 컨텍스트(Context)
        OS의 스케줄러에 의해 시작될 프로세스와 그 환경에 대한 정보 집합 을 말함
        정보집합 : 정적변수, 동적 변수를 사용하는 실행 코드, 스택, 레지스터, 프로그램 카운터

     - 스레드는 프로세스의 주소공간의 코드를 수행할 책임을 가진다.
       하나의 프로세스에 여러개의 스레드가 존재할 경우 스레드 들은 프로세스의 주소 공간을 공유.
     - 커널 오브젝트 핸들 테이블은 스레드별로 존재하는 것이 아니라 프로세스별로 존재하기 때문에,
       스레드들은 커널오브젝트 핸들을 공유


     - 프로세스는 자신만의 주소 공간을 가지기 때문에 많은 시스템 리소스를 사용
       (메모리, 파일 리소스...)
     - 스레드는 프로세스에 비해 적은 시스템 리소스를 필요로 함.
       (스레드 커널 오브젝트, 스레드 스택)


■ 스레드 함수의 동작


1. 모든 스레드는 수행을 시작할 진입점 함수(Entry-point function) 를 반드시 가져야 한다.
   - 프로세스 내에 두번째 스레드를 만들때 진입점 함수의 형태

DWORD WINAPI ThreadFunc(PVOID)
{
    DWORD dwResult = 0;
    ...
    return dwResult;
}




■ 프로세스, 스레드

위의 예는 하나의 프로세스가 두 개의 스레드를 갖었을 때 프로세스 주소 공간의 모습
- 두 스레드는 코드, 리소스, 전역 데이터와 환경 변수 등을 공유 한다.
- 스레드는 각각 자신의 스택을 독립적으로 가지고 있다.
- 프로세스와 스레드를 관리하기 위해 운영체제는 Kernel Object 를 생성 및 유지 한다.

2009년 3월 24일 화요일

프로세스ID를 이용한 전체 경로명(Full pathname) 얻기 -QueryFullProcessImageName()

프로세스ID를 이용한 전체 경로명(Full pathname) 얻기 - QueryFullProcessImageName()



 프로세스ID를 이용하여 전체 경로명을 얻기 위해 psapi.h 에 정의되어 있는
GetModuleFileNameEx 와 GetProcessImageFileName 등의 함수를 사용할 수 있다.
그런데 새로운 프로세스가 생성되었음을 알려주는 통지 메시지가 도착했을 때 이러한
함수를 호출하면 프로세스의 주소 공간(Address Space)이 완전히 초기화되지 않은
상황(모듈들이 메모리 상에 매핑되기 전)일 수도 있기 때문에 함수 호출이 실패할 수도 있다.


GetProcessImageFileName()위의 특수한 상황 에서도 정확하게 전체 경로명을 획들할 수 있다.
하지만 경로명이 c:\Windows\System32\notepad.exe 와 같은 모습이 아니라
\Device\Harddisk\Volume1\Windows\System32\notepade.exe 와 같은 모습을 가진다.


QueryFullProcessImageName()어떠한 상황에서도 전체 경로명을 정확하게 획들할 수 있다.




cf) Windows VIA C/C++  - 한빛 미디어

2009년 3월 23일 월요일

[VS 2008] Linking 타임 자세한 정보 얻기

cf ) http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=8266&page=1



Linking 타임에 자세한 정보 얻기



링크시 include 되는 헤더파일의 순서에 따른 경고와 에러 메시지는 알아보기가 쉽지가 않다.
해당 경고와 에러를 해결하는 방법으로 프로그래머가 작성한 코드를 되집어 보는게(콜 스택) 효율적이다.
하지만 기억력에 의지하는 것도 한계가 있으므로 디버그 옵션을 주어서 찾는 방법도 있다.

/verbose:lib

디버그 옵션 설정





출력 예




/verbose 뒤에 옵션을 주어 출력 내용을 필터 할 수 있다.

CString - operator LPCTSTR () const;

CString - operator LPCTSTR () const;



const char *pName 과 같은 매개변수에 CString 객체 값을 대입하면 아무 문제 없이 동작한다.
const char * 와 CString 데이터 형은 분명 다르지만 명시적인 타입 캐스팅 없이 동작함을
무의식적으로 외우다 싶이 사용해 왔다.

즉,
int SomeFunc(char *pszInput);

CString str = "Test";
SomeFunc(src);


이렇게 사용할 수 있는 이유는
CString 클래스에 아래와 같이 LPCTSTR 자료형이 재정의(Overriding) 되어 있기 때문이다.

class CString
{
   ...
   operator LPCTSTR () const;   ...
};

2009년 3월 15일 일요일

[VS 2008] Visual C++ 2008 서비스팩1에 대해 알아보자(신경준) - 스크랩



출처 : http://www.zdnet.co.kr/ArticleView.asp?artice_id=00000039173306


[지디넷코리아]
마이크로소프트가 올 봄에 Visual C++ 2008 한글버전을 발표했다. 그러나 필자와 같이
Visual C++을 주로 사용하는 개발자들은 새로운 버전에 실망을 했을지도 모르겠다.

왜냐하면 Visual C++ 6.0 버전을 발표한 이후 지난 10년 동안 큰 변화가 없었는데, 이번 Visual C++ 2008 버전에서도
이렇다 할 큰 변화가 없었기 때문이다. 솔직히 Visual C++ 6.0에서 프로젝트를 만들어서 최신 Visual C++ 2003 혹은
Visual C++ 2005로 변환(포팅)해도 별문제가 없다.

버전이 달라졌어도 기본적으로 크게 달라진 점이 없기 때문에 별다른 문제가 발생하지 않는 것이다.

그랬던 Visual C+ 2008이 서비스 팩 1 발표를 통해 많은 변화가 발생할 것 이라고 마이크로소프트에서
자신 있게 말하더니, 드디어 지난 8월말에 Visual C++ 2008 서비스 팩 1 정식 버전을 발표했다. 그러면
무엇이 어떻게 변경되었는지 직접 확인해 보자.

10년 만에 2배로 커진 MFC
기존의 MFC보다 소스 코드가 2배 이상 커졌다. 추가된 MFC 소스코드 대부분은 UI 쪽이다. 드디어
Office 2007처럼 리본(Ribborn) UI를 쉽게 개발 할 수 있게 된 것이다. Visual C++ 2008 서비스 팩 1에
 새로 추가된 MFC로 그 동안 부족했던 Modern UI Look & Feel을 만들 수가 있게 됐다.

[새로 추가된 MFC 기능]
-오피스 리본 스타일 Interface 추가
-Visual Manager 추가 (Office look & feel)
-Visual Studio 스타일 Docking ToolBar, Panes
-Office 스타일 메뉴
-사용자 정의 툴바와 메뉴
-고급 GUI 컨트롤
-MDI Tab 과 Group 지원

위에서 나열한 기능 덕분에 클릭 몇 번만으로 아래 화면과 같은 멋진 리본 UI를 만들 수 있다.


이 글을 읽고 있는 독자들도 서비스 팩 1에서 같이 제공하는 Sample Code를 보면 이 기능을 쉽게 사용할 수 있다.

C++ 라이브러리 TR1(Technical Report 1)
이미 많은 개발자들은 기존 STL의 한계 때문에 BOOST 라이브러리를 많이 사용하고 있을 것이다. TR1은 좀 더
간단하게 설명하면 STL의 업그레이드로 생각하면 된다. 그리고 C++ 위원회에서 인정하는 라이브러리이다.

즉, 특정 컴파일러에서만 지원하는 라이브러리가 아닌 C++ 컴파일러라면 무조건 지원해야 하는 라이브러리인
것이다. 그리고 C++개발자라면 당연히 알고 있어야 하는 라이브러리인 것이다. 참고로 TR1 이름에서 알고 있듯이
 곧 TR2, TR3… 이런 식으로 계속해서 나올 예정이다.

TR1(Technical Report 1)이란?

1. C++ 라이브러리 작업 그룹이 작성한 “1차 기술 보고서(Technical Report 1)“
2. 확장된 C++. 버전으로 이야기 하면 C++ 1.1
3. 현재는 TR2 논의 중 (http://www.open-std.org/jtc1/sc22/wg21/)
4. TR1 문서는 아래 사이트에서 다운 가능
(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1836.pdf)

cfile7.uf.18483F4450AEC1750CCAC2.pdf

Visual C++ 2008 ServicePack1에 추가된 라이브러리 목록은 다음 URL에서 확인 하면 된다.
(http://msdn.microsoft.com/en-us/library/bb982198.aspx)

필자는 TR1의 여러 라이브러리 중 Shared_Ptr에 대해서 간단하게 알아 보고자 한다.

auto_ptr ap(new int(123));

ASSERT(0 != ap.get());
auto_ptr ap2(ap); //(1)

ASSERT(0 != ap2.get());

ASSERT(0 == ap.get());
auto_ptr 에서는 (1) 소스 코드에서 ap에서 ap2로 포인터 소유권을 이전한다.
shared_ptr sp(new int(123));

ASSERT(0 != sp);
shared_ptr sp2(sp); //(2)

ASSERT(0 != sp2);

ASSERT(0 != sp);
TR1 라이브러리의 Shared_Ptr에서는 (2) 소스 코드 라인에서 공유 개체의 참조 횟수 증가하게 된다.
즉, 내부적으로 같은 리소스를 참조하는 모든 shared_ptr 개체는 하나의 제어 블록을 공유하며,
이 제어 블록은 리소스를 함께 소유하는 shared_ptr 개체의 수만 변경하게 된다. 또한 Shared_Ptr 외에
참조 개수를 올리지 않는 Weak_Ptr 라이브러리도 제공한다.

필자가 .net에서 가장 부러워했던 것 중에 하나가 메모리 관리 기능을 .net framework에서 제공하는
것이다. 그런데 Visual C++ 2008 에서 제공하는 TR1을 이용해서 자신만의 메모리 관리 클래스를 만들어
Reference Count계산과 Garbages Collecting을 하면 어느 정도 메모리 관리를 할 수 있을 것 같다.
(실제로 구현하기는 꽤 힘들겠지만 해볼만할 것 같다.)

Visual C++ 2008 서비스 팩 1은 분명히 Visual C++ 라이브러리에 있어 환영할 만한 업그레이드이며
유용하게 사용할 수 있다. 이 글에서 다루지 못한 부분도 많지만 소개된 일부 내용들이 많은 개발자의
흥미를 끌어 직접 살펴볼 수 있는 계기(자극제)가 되기를 기대해본다.


필자는 현재 안철수연구소 선임연구원으로 재직 중이다. 비주얼 C++ 전문가 MVP이며,

다수의 세미나와 기고 등 활발히 활동 하는 개발자다.

2009년 3월 7일 토요일

AfxGetApp() 와 AfxGetMainWnd()

윈도우즈 프로그램의 기본적인 실행 구조는 다음과 같다.

실행 파일을 실행 시키면 하나의 프로세스가 생성 된다.
프로세스는 단독으로 수행될 수 없고 스레드에 의해 수행된다.
스레드는 프로세스의 메모리 공간(가상 주소 공간) 을 수행할 책임을 갖게 된다.
프로세스는 하나 의상의 스레드를 갖으며, 최초의 스레드를 Primary Thread 라고 부른다.

MFC로 만든 응용프로그램 역시 윈도우즈 프로그램이므로, 하나 이상의 스레드를 갖는다.
Primary Thread 역할을 하는 것은 CWinApp 클래스의 파생 클래스인 CXXApp 클래스가 스레드를 생성한다.
자세히 말하면 MFC 응용프로그램의 유일한 전역 객체인 CxxApp의 객체가 진입점 함수(WinMain, wWinMain) 함수를
실행시키게 된다. (진입점 함수는 C/C++ 런타임 시작함수가 호출 한다.)


■ AfxGetApp()
AfxGetApp() 전역 함수를 호출하게 되면 MFC 응용프로그램의 최초에 생성된 스레드의 app 를 반환해 준다.
반환 타입이 CWinApp * 이므로 사용할때는 자신이 사용할 타입으로 적절히 타입 캐스팅 해주어야 한다.

 메인 스레드의 메인 윈도우 객체 포인터를 얻기 위해선
::AfxGetApp()->m_pMainWnd;
와 같이 m_pMainWnd 멤버 변수 값을 참조 하면 된다.
또는
::AfxGetApp()->GetMainWnd();
멤버 함수를 호출하여 값을 얻을 수도 있다.


■ AfxGetMainWnd()
 AfxGetMainWnd()  MFC 전역 함수는 현재 스레드의 메인 윈도우 핸들을 리턴한다.
단일 스레드 프로그램의 경우 ::AfxGetApp()->GetMainWnd();  와 같은 결과를 얻을 수 있다.
하지만 멀티 스레드 프로그램일 경우에는 주의해서 사용 해야 한다.
AfxGetMainWnd() 의 원형은 다음과 같다.

_AFXWIN_INLINE CWnd *AFXAPI AfxGetMainWnd()
{
    CWinThread *pThread = AfxGetThread();
    return pThread != NULL ? pThread->GetMainWnd() : NULL;
}


리턴값을 보면 현제 스레드의 메인 윈도우를 리턴한다.
따라서 다른 쓰레드에서 AfxGetMainWnd() 함수를 사용하면 다른 윈도우 핸들을 리턴할 가능성이 있다.

다른 스레드에서 메인 윈도우 핸들을 얻기 위해선
::AfxGetApp()->GetMainWnd();
함수를 사용하면 된다.

2009년 3월 1일 일요일

[ListCtlr] 선택한 아이템 얻기 - 2 (LVN_ITEMCHANGED)

선택한 아이템 얻기 - 2 (LVN_ITEMCHANGED)




 리스트 컨트롤에서 특정 아이템을 선택 한 후
MainFrame 의 메뉴에서 리스트에 선택된 아이템 텍스트를 얻어와야 했다.
즉, 리스트 컨트롤에는 특정 아이템이 선택되어진 상태 이고, 추후에 다른 뷰나 클래스에서
리스트 컨트롤에 선택된 아이템의 정보를 얻는 것 이였다.

 방법1)
 선택된 아이템만을 얻고자 한다면 다음 코드를 참고 하면 된다.

for (int i = 0; i < m_list.GetItemCount(); i++)
{
    if (m_list.GetItemState(i, LVIS_SELECTED) & LVIS_SELECTED)
    {
        CString m_sTest;
        m_sTest.Format( "%d\n",i);
    }
}

또는

CListCtrl* pListCtrl = (CListCtrl*) GetDlgItem(IDC_YOURLISTCONTROL);
ASSERT(pListCtrl != NULL);
POSITION pos = pList->GetFirstSelectedItemPosition();

if (pos == NULL)
   TRACE0("No items were selected!\n");
else
{
   while (pos)
   {
      int nItem = pList->GetNextSelectedItem(pos);
      TRACE1("Item %d was selected!\n", nItem);
      // you could do your own processing on nItem here
   }
}


방법2).
 나의 코드에는 리스트 컨트롤의 아이템이 변경 될 경우 발생하는 메시지인 LVN_ITEMCHANGED 의 핸들러를 구현해 놨다.
차라리 위의 방법처럼 조사하고자 할 때 모든 리스트 컨트롤의 아이템을 조사 하지 않고,
LVN_ITEMCHANGED 의 핸들러에서 선택된 아이템을 멤버변수에 저장한 후 나중에 이 멤버함수의 값을 리턴 시키는 형태를 취했다.
이때, 주위할 점은 리스트 컨트롤에 출력되는 리스트가 한 종류가 아니라 다른 종류도 출력이 된다면 언제 그 멤버변수를
초기화 해야 하느냐는 신중히 고려 해야 한다.

void ChRmtEvwrView::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);

    if (!(pNMLV->uChanged == LVIF_STATE && pNMLV->uNewState == (LVIS_SELECTED | LVIS_FOCUSED)))
        return;

    INT nSelectItem = pNMLV->iItem;
    if (-1 == nSelectItem)
        return;

    if (LOGINFO_MODE == m_uListOutputMode)
    {
        CMainFrame   *pMainFrame = reinterpret_cast<CMainFrame *>(::AfxGetMainWnd());
        ChRmtEvwrDoc *pDoc       = GetDocument();
        if (NULL == pMainFrame || NULL == pDoc)
            return;

        CString sLogName = _T("");
        sLogName = GetListCtrl().GetItemText(nSelectItem, 0);
        m_sSelectedLogName = sLogName;
    }

*pResult = 0;

}