생각하는 족족 고.따.구 냐..

Posted
Filed under About Knowledge/Programs_C++

한용희 님의 글에서 발췌 합니다.

항상 테스트를 어떻게 진행해야 할까 고민을 많이 했었습니다. 툴은 가지고 있긴 한데, 이용하질 못 했네요.

한용희 님께 감사드립니다.


월간 마이크로소프트웨어 2007년 5월호

---------------------------------------------------------------------------------------------------------------


비주얼 스튜디오를 이용한 테스팅


'IT' 한국 발목잡는 부실 SW

보험금 과다 청구에 무더기 반품 사태도 일으켜

말썽나도 업계에선 쉬쉬

저가 경쟁에 검증 안거쳐

외국선 “한국산 못 믿어”

- 2007년 3월 28일 중앙일보 경제면 1면 기사


한용희 woom33@korea.com

롯데정보통신 정보기술연구소에 재직 중이며, 닷넷 기반의 여러 프로젝트에 참여했다. 현재 Microsoft Visual C# MVP이며 MSDN 세미나 강사로도 활동 중이다. 처음에는 2D,3D 게임 프로그래머로 시작하여 SQLServer 튜닝, 응용 애플리케이션 개발에 이르끼 까지 다양하게 경험하였으며, 주요 관심사는 DB와 애플리케이션의 연동부분 이다.


아래는 2007년 3월 28일 중앙일보 경제면 1면에 실린 기사의 일부이다.


------------------------------------박스 시작--------------------------------------------

D보험사는 지난해 말 수천여 명의 고객에게 약정된 보험료보다 더 많은 돈을 청구했다. 이 회사 금융전산망에 실린 소프트웨어(SW)가 말썽을 부려 빚어진 일이다. D사는 이 사실을 감춘 채 30여억원을 들여 일주일 동안 전산시스템을 샅샅이 조사했다. 그러나 SW 에러를 끝내 찾지 못했다. 이 회사 한 직원은 "회사 이미지가 떨어질까봐 선의의 피해자에게 몰래 보상해 입막음을 했지만 언제든지 이런 일이 재발될 가능성이 있다"고 말했다.


S정보기기는 부실 SW의 후폭풍으로 경영난을 겪고 있다. 이 회사는 지금까지 휴대용 정보기기를 800억원어치 팔았지만 제품에 쓰인 SW가 제대로 작동하지 않아 팔린 제품 대부분이 반품됐다.

------------------------------------박스 끝----------------------------------------------

왜 이런 일이 벌어지는 것일까? 개발 비용과 시간을 줄이다 보니 테스트를 제대로 하지 않고 출시한 SW가 많기 때문이다. 이런 관행은 국내 SW사가 외국에 나가서도 여전히 되풀이 되고 있다. 일본의 IT붐을 타고 진출한 한국의 SW업체 중 1/5 정도는 부실SW 문제가 불거져 현지 시장에서 퇴출을 당했다고 한다.


그렇다면 외국에서는 어느 정도의 예산을 테스트에 투자를 할까? 미국 Microsoft사의 경우 제품 개발비에서 60%까지 제품 테스트 비용으로 할당 한다고 한다. 하드웨어 업체의 경우도 점점 내장SW에 대한 투자 비용이 높아지고 있다. 일본 파나소닉의 경우 TV 제조원가의 절반을 내장SW 개발에 투자한다. 또한 북미 자동차 업체들은 연간 품질 보증 비용으로 100억 달러는 쓰는데 이중 30~40%는 전자기기 및 SW 고장 수리에 사용된다고 한다.


이처럼 SW의 신뢰성은 매우 중요하지만 한국의 현실에서는 비용과 시간에 쫒겨 많이 간과되고 있는 실정이다. 이 간과된 비용은 고스란히 유지보수 비용으로 발생하거나 나중에 시스템을 재개발 하기도 한다. 이러한 문제점을 극복하고자 몇 년 전부터 유행하는 것이 바로 테스트 주도 개발(Test Driven Development)이다. TDD에서 가장 핵심적인 요소는 바로 신뢰성이다. TDD를 통해서 코드의 신뢰성을 보증하자는 것이다. TDD의 또다른 장점은 바로 디커플링(decoupling)이다. 리팩터링을 용이하게 하기 위해서는 코드가 디커플링이 되어 있어야만 쉽게 변경 할 수 있다. 예를 들어 요구 사항의 변화가 있어 코드를 수정하는 경우나 코드의 품질을 높이기 위해 리팩터링 하는데 있어 어느 한 부분을 수정하였더니 다른 부분까지 문제가 되는 경우, TDD 방법을 통한 단위 테스트가 되어 있지 않다면 어디에 문제가 있는지 찾는 것이 쉽지 않다. 이러한 문제를 찾기 위해서는 코드를 하나하나 뜯어 봐야 한다. 하지만 TDD 방법론에 의한 단위 테스트 코드가 작성되어 있다면 이를 테스트 해보는 것 만으로도 어디에 문제가 있는지 쉽게 찾을 수 있다. 또한 이러한 단위 테스트 자체가 코드의 디커플링을 유발하는 효과까지 가져온다. 이 TDD에 대해서는 앞 코너에서 상세히 다루었으므로 이번 코너에서는 Microsoft의 비주얼 스튜디오를 이용한 실제 활용 방법에 대해 알아볼 것이다.


비주얼 스튜디오 팀 시스템 소개

과거 비주얼 스튜디오는 개발자를 위한 개발도구였다. 그러나 한 프로젝트를 하기 위해서는 프로젝트 매니저, 비즈니스 분석가, 애플리케이션 아키텍쳐, 개발자, 테스터와 같은 많은 사람들이 참여를 하게 되었고 이들은 서로 다른 도구를 사용해서 프로젝트를 진행하였다. 그러다보니 서로 다른 도구들은 통합이 안되어서 중복된 작업을 하게 되고 의사소통 문제가 불거지게 되면서 전체 프로젝트 개발 비용에서 무시할 수 없는 수준에 까지 이르렀다. 이제는 더 좋은 개발 도구를 만든다고 프로젝트의 개발 생산성이 향상 되는 것이 아니라, 프로젝트에 관계된 다른 사람들까지 고려를 해야만 전체적인 개발 생산성이 향상된다는 것을 Microsoft는 깨닫게 되었다. 그래서 나온 것이 바로 비주얼 스튜디오 팀 시스템이다. 제품명에서도 알수 있듯이 이제는 개발 도구(tool)가 아닌 시스템(system)이다.


팀 시스템을 여러 제품군으로 나누어져 있는데, 각 제품군별 구성은 <그림1>과 같다.


<그림1> 비주얼 스튜디오 팀 시스템 제품군

User inserted image


<그림1>을 보면 개발자와 테스터는 단위 테스트 도구(Unit Testing)와 코드 검사 도구(Code Coverage)가 함께 포함이 되어 있는 것을 볼 수 있을 것이다. 이는 개발자도 단위 테스트와 코드 검사를 해야 하기 때문이다. 이제 TDD 방법론을 이용한 비주얼 스튜디오의 개발 과정을 예제를 통하여 알아 보자.


클래스 디자인

이번 예제에서는 문자열 클래스를 하나 만들 것이다. 문자열을 삽입하고 삭제하는 기능을 가진 단순한 클래스이다. 먼저 클래스 디자인을 해보자. 비주얼 스튜디오에서 “클래스 라이브러리” 프로젝트를 만들고 만들어진 프로젝트의 클래스 파일에서 마우스 오른쪽 버튼을 누르면 “클래스 다이어그램 보기” 버튼이 나온다. 이를 누르면 직접 클래스 다이어그램을 그릴 수 있다. 이 다이어그램은 해당 코드 파일과 실시간으로 연동이 되는데, 다이어그램을 수정하든 코드를 수정하든 모두 양방향으로 실시간으로 업데이트가 된다. 먼저 다이어그램을 이용하여 <화면1>처럼 디자인을 해보자. 클래스의 세부 내용을 수정하기 위해서는 클래스 다이어그램에서 마우스 오른쪽 버튼으로 “클래스 세부 내용” 메뉴를 누르면 세부 내용을 편집 할 수 있다.


<화면1> 문자열 클래스 디자인

User inserted image

이렇게 디자인을 한 후 실제 코드를 보면 <리스트1>처럼 실제 구현 부분을 제외한 나머지 뼈대 부분이 자동으로 생성되어 있는 것을 볼 수 있다. 실제 구현 부분에는 아직 구현이 안 되어 있다는 예외를 리턴하게 되어 있는데, 실제로 구현 할 때에는 그 부분을 삭제하고 내용을 코딩 하면 된다.


<리스트1> 문자열 클래스 디자인한 결과 코드

namespace StringSetLibrary

{

   public class StringSet

   {

      private string _fullString;


      public StringSet(string initialWord)

      {

         throw new System.NotImplementedException();

      }


      public string fullString

      {

         get

         {

            throw new System.NotImplementedException();

         }

      }


      public void Insert(string word)

      {

         throw new System.NotImplementedException();

      }


      public void Delete(string word)

      {

         throw new System.NotImplementedException();

      }

   }

}


구현보다 테스트를 먼저!, 단위 테스트(Unit Test)

이제 클래스를 디자인 하였다면, 클래스의 내용을 구현하는 것이 일반적인 방법일 것이다. 하지만 TDD에서는 테스트 케이스부터 만들고 실제 구현을 한다. 클래스가 완성이 되었다면 통과해야 할 테스트 케이스를 먼저 만든 다음에 실제 구현을 하는 것이다. 사실 이런 테스트 케이스를 만들다 보면 오히려 클래스를 전체적으로 바라보게 되므로 클래스 디자인시에는 미처 발견하지 못했던 부분을 찾을 수 있어서 디자인을 더 자세하게 수정 할 수 있다. 즉, 구현하기 전에 미리 검증을 하므로 디자인을 더욱 완벽하게 할 수 있는 것이다. 이것이 구현 코드까지 만든 후에 디자인을 바꾸는 것 보다 몇배는 더 쉬울 것임은 의심할 여지가 없을 것이다. 그렇다면 이제 단위 테스트를 만들어 보자. 비주얼 스튜디오에서는 단위 테스트를 위한 기본적인 코드를 자동으로 생성해 주는 편리한 기능을 제공한다. 클래스의 코드에서 클래스 구문위에 마우스를 올려 놓고 오른쪽 버튼을 누르면 “단위 테스트 만들기”라는 메뉴가 나온다. 이를 눌러서 실행하면 기본적인 단위 테스트 코드의 뼈대가 만들어 진다.


단위 테스트를 위한 클래스는 클래스 속성으로 [TestClass()]라고 표기를 해야만 한다. 또한 각각의 단위 테스트 메소드도 메소드 속성에 [TestMethod()]라는 속성이 있어야만 테스트 메소드로 인식을 한다. 기본적으로 생성된 코드에서 테스트 케이스에 맞는 코드로 수정을 한다. 한 예로 문자열을 삽입하는 Insert 메소드의 단위 테스트 예제를 보자. <리스트2>는 자동으로 생성된 단위 테스트 코드이다.


<리스트2> 문자열 클래스에서 자동으로 생성된 삽입 메소드의 단위 테스트 구문

[TestMethod()]

public void InsertTest()

{

   string initialWord = null; // TODO: 적절한 값으로 초기화합니다.


   StringSet target = new StringSet(initialWord);


   string word = null; // TODO: 적절한 값으로 초기화합니다.


   target.Insert(word);


   Assert.Inconclusive("값을 반환하지 않는 메서드는 확인할 수 없습니다.");

}


우리의 예상은 삽입 메소드라면 문자열을 삽입한 이후 결과물이 합쳐진 상태로 나오게 하는 것이 목적이다. 따라서 위의 코드를 <리스트3>처럼 수정해 보자.


<리스트3> 문자열 클래스의 삽입 메소드에 대한 단위 테스트 코드

[TestMethod()]

public void InsertTest()

{

   string initialWord = "aaa"; // TODO: 적절한 값으로 초기화합니다.


   StringSet target = new StringSet(initialWord);


   string word = "bbb"; // TODO: 적절한 값으로 초기화합니다.


   target.Insert(word);


   Assert.AreEqual("aaabbb", target.fullString);

}


aaa 문자열에 bbb 문자열을 합치면 aaabbb가 되어야 한다. 이번에는 삭제 메소드에 대한 테스트 코드를 <리스트4>와 같이 만든다. 이런식으로 다른 메소드의 단위 테스트 코드도 완성을 한다.


<리스트4> 문자열 클래스의 삭제 메소드에 대한 단위 테스트 코드

[TestMethod()]

public void DeleteTest()

{

   string initialWord = "aaa"; // TODO: 적절한 값으로 초기화합니다.


   StringSet target = new StringSet(initialWord);


   string word = "aaa"; // TODO: 적절한 값으로 초기화합니다.


   target.Delete(word);


   Assert.AreEqual("", target.fullString);

}


단위 테스트 코드를 다 완성하면 이제 전체 단위 테스트를 해보자. 테스트 프로젝트를 선택한 후 비주얼 스튜디오 상단 메뉴에 보면 “테스트” 라는 메뉴가 있다. 그 하위 메뉴를 보면 “선택한 프로젝트를 디버거 없이 시작”이라는 메뉴가 있다. 이를 선택하면 테스트 디버거 없이 수행 할 수 있다. 물론 디버거 까지 같이 돌리면서 수행 할 수도 있다.

User inserted image

<화면2> 단위 테스트 결과

<화면2>를 보면 단위 테스트 결과가 나올 것이다. 이미 예상은 하였겠지만, 전부 실패다. 오류 원인은 아직 미구현이 되었다는 예외를 리턴했다는 것이다. 이제 실제 클래스의 내용을 <리스트5>처럼 채워 보자.


<리스트5> 문자열 클래스의 전체 구현 코드

public class StringSet

{

   private string _fullString;


   public StringSet(string initialWord)

   {

      _fullString = initialWord;

   }


   public string fullString

   {

      get

      {

         return _fullString;

      }

   }


   public void Insert(string word)

   {

      _fullString += word;

   }


   public void Delete(string word)

   {

      if (_fullString.IndexOf(word) < 0)

         throw new Exception("Cannot find word");

      _fullString = _fullString.Replace(word, "");

   }

}


삽입은 단순하게 문자열을 더하는 것이고 삭제는 단순하게 구현하기 위하여 해당 문자열을 공백으로 치환을 하였다. 이제 이 클래스 라이브러리를 빌드하고 테스트 프로젝트에서 테스트를 수행하면 4가지 메소드 테스트가 성공으로 끝나는 것을 확인 할 수 있을 것이다.


단위 테스트에 대한 검증, 코드 검사(Code Coverage)

단위 테스트를 잘 수행하였지만, 한가지 의문이 남을 수 있다. 단위 테스트 자체를 잘 만들었는지 검증을 해야 하는 것이다. 테스트 케이스를 빼먹으면 결국 테스트를 안 하는 것이 되기 때문이다. 따라서 단위 테스트 코드를 작성하는데에서 끝나면 안되고 그 단위 테스트 코드가 모든 클래스의 코드를 다 검증하는지 확인을 해야만 한다. 그때 사용하는 것이 바로 코드 검사(code coverage)이다. 이를 하기 위해서는 해당 솔루션에 보면 테스트 환경설정 정보를 설정해 주어야만 한다. 솔루션의 localtestrun.testrunconfig파일을 두 번 클릭하면 테스트 환경 설정을 하는 화면이 나오는데, 왼쪽의 “코드 검사” 메뉴를 누르고 오른쪽 화면에서 코드 검사를 하려는 DLL을 체크해 주면 된다. 우리는 문자열 클래스 라이브러리를 코드 검사할 것이므로 StringSetLibrary.dll을 선택하면 된다. 그러고 나서 다시 테스트를 수행한다. 수행 결과 창에서 마우스 오른쪽 버튼을 누르면 메뉴 중에 “코드 검사 결과”라는 메뉴가 있다. 이를 선택하면 코드 검사 결과를 볼 수 있다.


<화면3> 문자열 클래스에 대한 코드 검사 결과

User inserted image

<화면3>을 보면 전체 단위 테스트로 검사한 블록이 83.33%이고 검사 하지 않은 블록이 16.67%라고 나온다. 그렇다면 과연 어느 메소드에서 검사 안 한 부분이 있는 것일까? <화면3>을 자세히 보면 Delete 메소드만 검사율이 66.67%에 머물고 있다. 이를 더블 클릭하면 바로 해당 메소드를 이동하여 검사 안 한 부분을 빨간색으로 보여준다.


<화면4> 문자열 클래스의 삭제 메소드에서 단위 테스트를 안 한 부분

User inserted image

이에 대한 테스트 코드를 작성하면 코드 검사를 100%로 만들 수 있다.


 


코드 리뷰를 자동화한 정적 코드 분석(Code Analysis)

이제 단위 테스트를 통해서 모든 코드를 테스트 하였다. 하지만 테스트 코드를 모두 통과하였다고 모든 테스트를 완벽히 통과한 것은 아니다. 오류 라는 것은 단순 예측 결과물에 의한 테스트 뿐만 아니라 주변의 실행 환경이나 상황에 따라 많은 변수가 있기 마련이다. 이런 오류를 잡아내기 위해서는 코드를 분석해야 하는데 그 방법에는 두가지가 있다. 하나는 컴파일 이전에 코드를 하나하나 보면서 정적으로 분석하는 방법이 있고, 또 다른 방법으로는 코드를 컴파일 한 후 실행하면서 분석하는 동적 분석 방법이 있다. 먼저 정적인 방법은 보통 프로젝트에서 개발자가 만든 코드를 코드 리뷰라는 형식으로 분석하는 방법을 말한다. 하지만 이를 다 사람이 하기에는 시간이 많이 소요된다. 이를 어떤 규칙을 정해서 자동화 한 것이 바로 “코드 분석(Code Analysis)”이다. 과거 관리 코드(managed code)를 분석하는데 사용하였던 FxCop이라는 툴과 C/C++코드를 분석하는데 사용하였던 PREfast 툴을 합쳐서 코드 분석기라는 것으로 만들었다.

사용하는 방법은 어렵지 않은데, 분석하려는 프로젝트의 속성에 보면 “코드 분석”이라는 탭이 있다. 이를 선택한 후 “코드 분석 사용”이라는 체크 박스를 선택하면 코드 분석기가 작동하게 된다.


<화면5> 코드 분석기 화면

User inserted image

<화면5>에 보면 이미 미리 정의되어 있는 각종 규칙들이 있는데, 기본적으로 이를 어기면 “경고”창이 나오도록 설정이 되어 있다. 이를 오류가 나도록 변경하는 것은 해당 상태를 더블 클릭하면 된다. 또한 이러한 규칙은 프로젝트의 성격에 맞게 새로 만들 수도 있다. 일단, 다른 규칙은 제외하고 “명명 규칙” 하나만 테스트 해 보자.

문자열 클래스에서 “명명 규칙” 하나만 남기고 모든 규칙은 제외 한다. 그리고 다시 빌드를 하면 <화면6>과 같은 결과가 나온다.


<화면6> 코드 분석기를 적용하여 컴파일한 결과

User inserted image

문자열 클래스에서 프라이빗 필드인 _fullString에 접근하는 속성 명칭을 fullString으로 정의해서 나온 경고문구 이다. 이를 FullString이라고 대문자로 바꾸어 주면 이 경고 문구는 사라진다. 이처럼 기존에 사람이 하던 코드 리뷰를 코드 분석을 통해서 어느 정도는 자동화 할 수 있다.

 


성능 분석을 통한 동적 분석(Code Profiling)

정적 분석은 실제 실행하기 이전에 코드를 분석하는 방법이다. 이와 반대로 동적 코드 분석 기법인 코드 프로파일링(Code Profiling)은 실제 실행환경에서 코드의 성능을 분석하는 방법이다. SQL Server를 사용하신 분들은 SQL Profiler라는 툴을 잘 알 것이다. 이는 실제 실행환경에서의 쿼리 구문에 대한 분석을 가능하게 해 주는 툴이다. 코드 프로파일러도 이와 비슷한 기능을 한다.

이를 수행하기 위해서 실제 실행 환경을 만들어 보자. 문자열 클래스를 사용하는 간단한 콘솔 응용 프로그램을 추가하고 <리스트6>과 같은 코드를 실행해 보자.


<리스트6> 문자열 클래스의 응용 프로그램 코드

static void Test1()

{

   StringSet s = new StringSet("aaaaaaaaaa");


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

   {

      s.Insert("bbbbbbbbbb");

   }

   Console.WriteLine(s.fullString.Length);

}


기능은 간단하다. bbbbbbbbbb라는 문자열을 만번 반복적으로 추가 하는 것이다. 이제 이에 대한 성능을 측정하려면 비주얼 스튜디오 상단의 메뉴중 “도구”에서 “성능 도구”-“성능 마법사”를 클릭한다. 그러면 성능을 측정할 프로젝트를 선택한 후 프로파일링 방법을 물어 본다. 프로파일링 방법은 샘플링(sampling)과 계측(instrument)라는 두가지가 방법이 있다. 샘플링은 전체 응용프로그램을 일정 시간 간격으로 성능을 측정하는 방법이다. 일정 시간 간격으로 체크 하므로 부하가 적게 발생하지만 상세 정보는 알 수 가 없다. 반면에 계측 방법은 해당 응용 프로그램의 함수에 시작과 끝을 체크하는 구문을 삽입하여 정확한 성능을 측정하는 방법이다. 전체적인 시스템 성능을 보는데 있어서는 샘플링 방법이 좋고, 응용 프로그램의 각각의 함수 마다 정확한 정보를 보기 위해서는 계측 방법이 더 좋다. 여기에서는 계측 방법을 사용하자. “마침”을 선택하고 “성능 탐색기”에서 해당 성능 측적을 시작하면 <화면7>과 같은 결과가 나온다.


<화면7> 문자열 클래스의 성능 검사 결과

User inserted image

Insert 메소드가 만번 호출이 되었으며, 약 2.6초의 시간이 걸렸다는 결과가 나왔다. 각기 화면을 더블 클릭하면 호출 트리를 볼 수도 있으며 각 메소드별 상세 성능 정보도 볼 수 있다. 이는 닷넷 튜닝 예제로 많이 알려진 사례인데, string 타입은 불변(immutable) 타입으로써 문자열을 더하면 기존 문자열에 새 문자열을 더하는 것이 아니라, 문자열을 합친 후 기존 문자열은 버리고 새로운 문자을 생성한다. 왜냐하면 기존 문자열은 수정 할 수 없는 불변 타입이기 때문이다. 그렇기 때문에 메모리 할당하는데 부하가 걸리는 것이다. 이를 튜닝하기 위해서는 StrignBuilder라는 클래스를 이용하면 된다. 이는 문자열을 위해서 미리 버퍼를 할당하기 때문에 기존 문자열을 수정 할 수 있다.

기존 StringSet 클래스와 비교를 위해서 이번 클래슨 StringSet2로 하여 <리스트7>과 같이 만들어 보자.


<리스트7> 기존 StringSet 클래스를 개선한 StringSet2 클래스

public class StringSet2

{

   private StringBuilder _fullString;


   public StringSet2(string initialWord)

   {

      _fullString = new StringBuilder(initialWord);

   }


   public string fullString

   {

      get

      {

         return _fullString.ToString(); ;

      }

   }


   public void Insert(string word)

   {

      _fullString.Append( word);

   }


   public void Delete(string word)

   {

      _fullString = _fullString.Replace(word, "");

   }

}


이번에는 삽입 메소드에서 단순 더하기가 아닌 append 구문을 이용해서 구현하고 있다. 이제 이 둘을 동시에 테스트 해보자. 마찬가지로 테스트를 하고 각 메소드에 대한 상세 정보를 보면 <화면8>과 같다.


<화면8> 두 문자열 클래스의 삽입 성능 비교

User inserted image

<화면8>을 보면 두 클래스의 삽입 메소드의 성능 차이가 극명하게 보인다. 처음에 만든 것은 약 2.6초가 걸렸으며, 나중에 만든 것은 0.003초가 걸렸다. 이와 같이 성능 테스트를 통하여 미리 성능을 검증할 수 있다.


 


웹 애플리케이션을 위한 웹 테스트

웹 애플리케이션은 그 환경이 일반 응용 프로그램과 사뭇 다르다. 기본적으로 네트웍에 연결이 된 상태로 클라이언트 브라우저를 통해서 서버에 접속하게 된다. 따라서 이러한 환경을 위한 별도의 테스트 툴이 필요한데 비주얼 스튜디오에서는 웹 테스트라는 툴을 제공한다. 이 웹 테스트는 익스플로러에 삽입이 되어서 함께 작동을 하는데, 사용자의 모든 액션을 다 녹화를 한다. 그리고 그 녹화된 결과에 대해 유효성 검사나 결과 검사를 수행 할 수 있다.

먼저 예제로 Northwind DB의 직원을 검색하는 웹 프로그램을 만들어 보자. 새로운 솔루션에서 “새 웹 사이트”로 파일 시스템 형식의 웹 사이트를 만든다. 이 강좌는 ASP.NET 2.0 강좌가 아니므로 자세한 구현은 생략한다. 단, 테스트를 위해서는 웹 프로젝트의 포트를 고정시켜야 한다. 솔루션에서 웹 사이트를 누르면 속성 메뉴에 “동적 포트 사용”이라는 메뉴가 있다. 이를 False로 변경해서 고정 포트를 사용해야만 웹 테스트시에 그 포트 번호로 접속해서 테스트를 할 수 있다. 물론 웹 사이트를 파일 시스템이 아닌 HTTP를 이용해서 만든 경우에는 해당하지 않는 내용이다.


<화면9> NorthWind DB의 직원을 검색하는 웹 애플리케이션

User inserted image

<화면9>는 웹 애플리케이션의 디자인 뷰와 king이라는 사용자를 검색한 결과 화면 이다.이제 이 웹 애플리이션을 테스트 해보자. 새 테스트 프로젝트를 솔루션에 추가하고 새 항복으로 “웹 테스트”를 추가하면 익스플로러가 열린다. 여기에서 녹화하고자 하는 웹 페이지의 URL을 입력하면 자동으로 녹화가 시작된다. 이곳에 사용자 검색으로 king을 검색하는 행위를 하면 <화면10>과 같이 녹화가 된다.


<화면10> king을 사용자 검색한 화면을 녹화한 장면

User inserted image

이제 녹화한 이 행위를 수정해서 다른 테스트를 해 볼 수도 있다. 파라메터를 바꿔서 유효성 검사를 할 수도 있다. 예를 들면 화면에 “환영합니다”라는 단어가 꼭 나와야만 테스트를 성공한 것으로 간주한다고 하는 유효성 검사 조건을 줄 수도 있다. 또한 파라메터도 동적으로 바꿀 수 있는데, DB에서 파라메터를 읽어와서 동적으로 계속 바꿔 줄 수도 있다. 따라서 검색어로 king이 아닌, Fuller, Davolio와 같은 다양한 파라메터를 테스트 할 수 있다.


부하 테스트(Load Test)

한명의 사용자를 위한 테스트가 아닌 다수의 사용자를 동시에 테스트 하는 환경을 만들기 위해서는 부하 테스트를 해야만 한다. 특히 웹 애플리케이션의 경우 한 명이 아닌 다수가 동시에 접속하는 경우가 많기 때문에 부하 테스트는 필수적인 부분이다. 그렇다고 많은 사람을 불러다 놓고 특정 메뉴를 동시에 “자 누르세요~”라고 하기도 쉽지 않다. 따라서 이러한 행위를 대신 해주는 자동화 테스트 도구가 바로 부하 테스트 도구이다.

부하 테스트는 기본적으로 다른 테스트를 기반으로 테스트를 수행한다. 위에서 이미 만들어 둔 웹 테스트를 기반으로 부하 테스트를 해 보자. 새 항목으로 “부하 테스트”를 추가한다. 그러면 부하 테스트 마법사가 나온다.

첫 번째로 사용자를 어떻게 추가할 것인지 물어 본다. 일정한 수의 사용자로 부하 테스트를 할 수도 있고, 단계적으로 사용자 수를 늘릴 수도 있다. 두 번째로 어떤 테스트를 수행 할 것인지 묻는데 우리는 위에서 만든 웹 테스트를 수행 할 것이므로 해당 웹 테스트를 선택한다. 세 번째로는 브라우저를 선택하는 화면이 나오는데, 익스플로러 뿐만 아니라 넷스케이프, 스마트폰 브라우저까지 선택이 가능하다. 네 번째로는 네트워크 환경을 선택할 수 있는데, LAN, DSL, 전화 모뎀까지 다양한 환경을 선택할 수 있다. 마지막으로 어떤 컴퓨터의 성능 카운터를 수집할 것인지 컴퓨터의 이름을 적어 주면 된다. 이렇게 실제 수행 환경과 비슷한 경우를 <화면11>과 같이 만들 수 있다.


<화면11> 부하 테스트 설정 화면

User inserted image

여기에서 “실행 설정”의 속성 정보를 수정하면 SQL Server의 프로파일러도 추가하여 같이 추적할 수 있다. 따로 SQL Server의 프로파일러를 준비할 필요가 없는 것이다.

이제 부하 테스트를 수행해 보자. 부하 테스트를 수행하면 <화면12>와 같은 결과 화면이 나온다.


<화면12> 부하 테스트 결과 화면

User inserted image

<화면12>를 보면 사용자수는 단계적으로 계속 증가하는 화면이 보이며, 평균 응답 시간도 시간이 갈수록 전반적으로 나빠지고 있는 것을 확인 할 수 있을 것이다. 화면 왼쪽에 보면 성능 카운터를 확인 할 수 있다. 이중 빨간불이 들어온 것은 성능에 문제가 있는 부분이다. <화면12>에서는 안 보이는데, 트리를 펼쳐보면 CPU 사용량이 100%에 도달해서 나온 부분이다. 노란불은 경고 부분으로 이 부분 또한 잠재적인 위험성이 있는 부분이므로 주의 깊게 살펴 보아야 하는 부분이다.

이러한 성능 카운터 말고도 SQL Server 프로필러 정보도 동시에 확인이 가능한데, 이전에 프로필러 옵션을 선택했다면 해당 폴더에 프로필러 결과 파일을 만들어 준다. 이를 열어 보면 <화면13>과 같다.


<화면13> SQL Server 프로필러 결과 화면

User inserted image

결과를 보면 DB 서버에 직원 검색 쿼리를 계속 다른 파라메터로 날리고 있는 것을 확인 할 수 있을 것이다.


테스트를 위한 종합 선물 셋트

비주얼 스튜디오는 프로젝트에 관련된 모든 사람(프로젝트 매니저, 비즈니스 분석가, 애플리케이션 아키텍쳐, 개발자, 테스터 등)을 위한 통합된 도구를 제공한다. 테스트를 위해서도 각각의 목적에 맞게 따로 사용했던 단위 테스트, 코드 검사, 코드 분석, 코드 프로파일링, 부하 테스트 등과 같은 작업을 이제는 하나의 툴에서 통합적으로 사용할 수 있다. 또한 이렇게 테스트한 결과를 다른 프로젝트의 구성원과 쉽게 공유할 수 있도록 워크 아이템을 등록하여 손쉽게 공유도 가능하다.

비주얼 스튜디오의 이러한 기능은 참으로 편리한 기능이다. 하지만 서두에서 언급을 했듯이, 이러한 좋은 도구를 사용하기 위해서는 그에 걸맞는 개발 환경이 갖추어 져야만 한다. 개발 비용과 시간에 쫒겨서 테스트에 대한 고려를 충분히 하지 않는 다면 아무리 좋은 도구가 있어도 무용지물이 될 것이며, 결국은 부실한 SW를 양산하게 되는 결과를 초래하게 될  것이다.

2007/05/12 19:55 2007/05/12 19:55
Posted
Filed under About Knowledge/Programs_C++
아직도 잘 모른다..

서브클래싱...

실은. .프로그램을 잘 모른다..

시늉만 하는 중이지....

!@#$%^&*()_+1818#$%^&*()_181818
User inserted image
2007/04/02 01:42 2007/04/02 01:42
Posted
Filed under About Knowledge/Programs_C++
아래와 같은 글을 ["http://www.jiniya.net/tt/483"]에서 발견했다.

괴짜프로그래머의 일상사 라는 제목으로 운영되어지고 있는 블로그인데,

블로그 주인의 동의없이(??) 이래 저래 참 도움이 많이되는 곳이라 하겠다.

아래의 글을 역시나 주인의 동의 없이 잘 긁어다 붙였다.

===================================================================================================
신입 개발자를 위한
CRT 이야기
영진 pop@jiniya.net


CRT C Runtime Library 말한다. C Runtime Library 각종 C언어의 표준 함수들을 포함하고 있는 거대한 라이브러리다. printf, scanf, atoi, fgets등의 C언어를 처음 익힐 배웠던 함수들이 라이브러리에 모두 포함되어 있다. 기본적으로 Visual C++에는 가지 종류의 CRT 포함되어 있다. 단일 스레드(ML), 다중 스레드(MT), 다중 스레드 DLL(MD) 그것이다. 가지도 각각 릴리즈 버전과 디버그 버전이 존재하기 때문에 정확하게는 여섯 가지가 있는 셈이다.

동일한 함수를 구현하고 있는 라이브러리를 여섯 가지나 만들어둔 이유는 베스킨라빈스에 아이스크림이 서른 가지가 넘게 있는 이유와 동일하다. 그때그때 상황에 맞게 골라서 쓰기 위해서이다. 당연히 베스킨라빈스의 아이스크림이 모두 맛이 틀리듯이 CRT들도 종류에 따라서 미묘한 차이가 있다. < 1>에는 CRT 종류별 특징이 나와있다. 디버그 CRT 경우는 ASSERT 경고문이 포함되어 컴파일 되었다는 특징을 추가적으로 가지고 있다.
 

1 CRT 종류별 특징

CRT

라이브러리

특징

단일 스레드

libc.lib

단일 스레드 전용으로 설계된 CRT. 멀티 스레드에 비해서 빠르다. 스레드에 포함된 일부 함수들은 멀티 스레드에서는 정상적으로 동작하지 않는다. Visual Studio 2005부터는 이상 단일 스레드 CRT 지원하지 않는다.

멀티 스레드

libcmt.lib

포함된 모든 함수가 멀티 스레드 환경에서도 정상적으로 동작한다.

멀티 스레드 DLL

msvcrt.lib

멀티 스레드 CRT 동일하고 함수들이 별도의 DLL 존재한다. 따라서 CRT 공유하는 모듈이 많을 경우 용량을 줄일 있다. 별도의 DLL CRT 함수가 존재하기 때문에 프로그램을 배포할 CRT DLL 같이 배포해야 한다.


독립된
별도의 프로그램이라면 < 1> 나와있는 특징대로 프로그램의 특성에 맞는 CRT 사용하면 된다. 프로젝트의 옵션 대화상자에 있는 C/C++ 탭에서 코드 생성 부분의 런타임 라이브러리에서 적절한 CRT 선택한 다음 새로 컴파일, 링크하면 된다.

반면에 하나로 묶여서 실행되는 모듈이라면 문제는 조금 복잡해진다. 이러한 모듈의 대표적인 예로는 DLL 정적 라이브러리가 있다. DLL 실행 시간에, 정적 라이브러리는 링크 시간이 바인딩 되어서 결국은 프로그램(exe) 동일한 주소 공간에서 실행된다. 이러한 모듈의 경우 CRT 섞어서 사용하면 문제가 되는 경우가 많다.

가장 흔히 발생하는 문제는 라이브러리 충돌 문제다. 서로 다른 CRT 버전을 사용했기 때문에 링크 단계에서 라이브러리 간에 기호 충돌 문제가 발생한다. 경우에는 CRT 모두 일치하도록 라이브러리를 새로 구성한 다음 컴파일 하도록 한다. 라이브러리를 자신이 만들지 않은 경우에는 또한 여의치 않다. 경우에는 프로젝트 설정 대화상자에서 링크 부분에서 라이브러리 무시에 충돌이 나는 라이브러리를 추가한 다음 컴파일 하도록 한다. 종종 링크 순서 때문에 문제가 발생하기도 한다. 경우에는 무시 라이브러리에 모두 추가한 다음 링크 순서대로 추가 종속성에 적어두면 된다.

DLL 때문에 가장 많이 겪는 문제 중에 하나는 new/delete 문제다. DLL EXE 서로 각각 CRT 링크한 경우(싱글 스레드, 멀티 스레드)에는 서로 다른 힙을 사용한다. 따라서 DLL에서 new 것을 EXE에서 delete하거나, 반대로 EXE에서 new 것을 DLL에서 delete하는 경우에 문제가 생긴다. 경우엔 new/delete 위치가 동일하도록 프로그램 구조를 고치거나 아니면 DLL EXE 모두 멀티 스레드 DLL 사용하도록 설정해야 한다.

이러한 복잡한 문제들을 사전에 방지하기 위해서는 CRT 되도록 통일해서 사용하고, DLL 경계에서 new/delete 하지 않도록 프로그램을 작성하는 것이 좋다.

2007/03/31 11:49 2007/03/31 11:49
Posted
Filed under About Knowledge/Programs_C++
어떤분의 글에서 얻은 이미지 한장.

생에 광을 좀더 해 줄 만한 책들이라 하겠다.

학교 다닐때 이미 깨달았으면 얼마나 좋았을고.....

나이 서른에 똥줄이 탈만도 하지...
User inserted image
2007/03/22 11:14 2007/03/22 11:14
Posted
Filed under About Knowledge/Programs_C++

API 프로그래밍에 대한 Q&A입니다.

1. 특정 디렉토리 뒤지기

지정한 디렉토리에 있는 모든 파일을 찾아내는 코드를 만들려면 어떻게 해야 합니까 ?

이 때 사용할 수 있는 API가 바로 FindFirstFile과 FindNextFile, FindClose라는 API들입니다. 사용 예제는 다음과 같습니다.

	WIN32_FIND_DATA  findFileData;
HANDLE hFileHandle;

// szDir에 뒤지고자 하는 디렉토리의 경로명을 준다. 예를 들면 "C:\\TEMP\\*.*"
// 찾아진 파일의 속성은 findFileData의 dwFileAttributes를 살펴본다.
hFileHandle = FindFirstFile(m_szDir, &findFileData);
if (hFileHandle != INVALID_HANDLE_VALUE) // 파일을 찾은 경우
{
// 찾은 파일의 이름은 cFileName 필드로 들어온다.
...
// 다음 파일을 찾는다.
while(FindNextFile(hFileHandle, &findFileData)) {
...
}
FindClose(hFileHandle);
}

2. API를 이용하는 유니코드와 ANSI 문자열간의 변환 방법

API를 이용해서 유니코드와 ANSI 문자열간의 변환은 어떻게 수행합니까 ?

Visual C++에서 유니코드 문자열은 BSTR이란 타입으로 표시됩니다. 또 유니코드와 ANSI 문자열간의 변환을 위해서 윈도우 시스템에는 MultiByteToWideChar와 WideCharToMultiByte라는 API가 존재합니다. MFC에서의 BSTR 타입 변환방법이나 ATL로 하는 BSTR 타입 변환도 참고하시기 바랍니다.

  • ANSI 문자열에서 유니코드로의 변환 방법
    	// sTime이란 ANSI 문자열을 bstr이란 이름의 유니코드(BSTR 타입) 변수로 변환
    char sTime[] = "유니코드 변환 예제";
    BSTR bstr;
    // sTime을 유니코드로 변환하기에 앞서 먼저 그 길이를 알아야 한다.
    int nLen = MultiByteToWideChar(CP_ACP, 0, sTime, lstrlen(sTime), NULL, NULL);
    // 얻어낸 길이만큼 메모리를 할당한다.
    bstr = SysAllocStringLen(NULL, nLen);
    // 이제 변환을 수행한다.
    MultiByteToWideChar(CP_ACP, 0, sTime, lstrlen(sTime), bstr, nLen);
    // 필요없어지면 제거한다.
    SysFreeString(bstr);
  • 유니코드에서 ANSI 문자열로의 변환 방법
    	// newVal이란 BSTR 타입에 있는 유니코드 문자열을 sTime이라는 ANSI 문자열로 변환
    char *sTime;
    int nLen = WideCharToMultiByte(CP_ACP, 0, newVal, -1, sTime, 0, NULL, NULL);
    sTime = malloc(nLen+1);
    WideCharToMultiByte(CP_ACP, 0, newVal, -1, sTime, 128, NULL, NULL);
    // 필요없으면 메모리를 제거한다.
    free(sTime);
  • 유니코드 문자열을 UTF-8으로 변환하기
         WideCharToMultiByte 함수를 호출할 때 첫 번째 인자로 CP_UTF8을 지정하면 된다. UTF-8은 유니코드의 인코딩 스킴 중의 하나로 쉽게 말하자면 문자열 스트림에서 0을 빼고 표현하는 방법이라고 볼 수 있다.

    3. 레지스트리 읽기/쓰기

    API를 이용해서 레지스트리에 한 항목을 생성하거나 기존 항목의 값을 읽어들이려면 어떻게 해야합니까 ?

    레지스트리 관련 API를 사용하려면 winreg.h라는 헤더 파일을 소스에 포함해야 합니다. 레지스트리에 키를 생성하는 방법과 레지스트리에 존재하는 키의 값을 읽는 방법을 차례로 살펴보겠습니다.

  • 레지스트리 키 생성 예제
    	// 예를 들어 HKEY_LOCAL_MACHINE밑의 System\CurrentControlSet\Services\GenPort라는 키를
    // 생성하고 거기에 DWORD 타입의 값으로 Type을 만들고 문자열 타입의 값으로 Group
    // 을 만들어 본다.
    #include "winreg.h"
    LONG error = 0;
    HKEY hKey;
    DWORD dwDisp, dwData;
    char lpData[] = "Write this down";

    // 먼저 만들려는 키가 이미 존재하는 것인지 살혀본다.
    error = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Services\\GenPort",
    0, KEY_ALL_ACCESS, &hKey);

    if (error != ERROR_SUCCESS) // 없다면 새로 생성한다.
    {
    // 키를 생성한다.
    error = RegCreateKeyEx(HKEY_LOCAL_MACHINE,
    "System\\CurrentControlSet\\Services\\GenPort", 0, "REG_BINARY",
    REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &dwDisp);
    // 위의 키 밑에 Type이란 DWORD 타입의 값을 만들고 1로 초기화
    dwData = 0x1;
    error = RegSetValueEx( hKey, "Type", 0, REG_DWORD,&dwData,4);
    // 위의 키 밑에 Group이란 문자열 타입의 값을 만들고 lpData의 값으로 초기화
    error = RegSetValueEx( hKey, "Group", 0, REG_SZ, lpData, strlen(lpData));

    // 키를 닫는다.
    RegCloseKey(hKey);
    }
  • 기존의 레지스트리 키에서 값 읽기
    	// HKEY_CURRENT_USER\Software\Netscape\Netscape Navigator\Main 밑의 Install Directory
    // 값의 문자열 값을 읽어들인다.
    DWORD dwType, cbData;
    HKEY hSubKey;
    long lRet;
    char pszString[255];

    // 키를 오픈한다.
    if ((lRet = RegOpenKeyEx(HKEY_CURRENT_USER,
    "Software\\Netscape\\Netscape Navigator\\Main",
    0, KEY_READ | KEY_QUERY_VALUE , &hSubKey)) == ERROR_SUCCESS)
    {
    cbData = 255; // 문자열 값을 읽어올 데이터의 크기를 준다.
    if ((lRet = RegQueryValueEx(hSubKey, "Install Directory",
    NULL, &dwType, pszString, &cbData)) == ERROR_SUCCESS)
    {
    // 제대로 읽힌 경우
    }
    else
    {
    // 에러가 발생한 경우
    }
    RegCloseKey(hSubKey);
    }
  • 레지스트리 키 삭제하기 - RegDeleteKey 함수를 사용한다.

    4. 윈도우 탐색기로부터의 Drag&Drop을 받으려면

    윈도우 탐색기로부터 제가 만든 윈도우로의 drag&drop이 가능하게 하려면 어떻게 해야 합니까 ?

    다음 순서를 따라서 프로그래밍하시면 됩니다.

    1. 프로그램의 초기화시에 DragAcceptFiles(hWnd, TRUE) 함수를 호출한다. 첫 번째 인자인 hWnd는 드롭의 타겟이 되는 윈도우의 핸들이다.
    2. 탐색기로부터 파일이 드롭되는 순간에 WM_DROPFILES 메시지가 날라온다. 이를 처리한다.
      	case WM_DROPFILES :
      {
      POINT pt;
      // 어느 위치에 드롭되었는지 그 항목을 알아낸다.
      if (DragQueryPoint((HDROP)wParam, &pt))
      {
      UINT i = 0;
      // 모두 몇 개의 파일이 드롭되었는지 알아낸다.
      // 만일 폴더가 드롭되었다면 폴더의 이름만 넘어온다.
      UINT uCount = DragQueryFile((HDROP)wParam, 0xFFFFFFFF, NULL ,0);

      for(i = 0;i < uCount;i++)
      {
      // 드롭된 파일의 이름을 알아온다.
      DragQueryFile((HDROP)wParam, i, buffer ,255);
      // 드롭된 파일 이름을 출력해본다.
      MessageBox(hWnd, buffer, "File Name", MB_OK);
      }
      }
      // drag and drop 작업을 끝낸다.
      DragFinish((HDROP)wParam);
      break;
      }
    3. Drag&drop을 더 사용할 필요가 없어지면 DragAcceptFiles를 호출한다.
      	DragAcceptFiles(hWnd, FALSE);

    5. 시스템의 모든 드라이브 알아내기

    현재 시스템에 붙어있는 모든 드라이브(네트웍 드라이브 포함)에 대한 정보를 알아내고 싶습니다.

    1. GetLogicalDriveStrings로 시스템에 마운트되어있는 모든 드라이브 정보를 알아낸다. 두 번째 인자인 buffer로 드라이브 정보가 들어오는데 그 구조는 c:\,d:\과 같은 형식이며 리턴값으로 그 버퍼의 크기가 들어온다.
      	char buffer[256];
      DWORD dwRet;
      LPSTR token;

      dwRet = GetLogicalDriveStrings(256, buffer);
    2. 루프를 돌면서 드라이브별 정보를 알아낸다. 이 때는 GetVolumeInformation 함수를 이용한다.
      	token = buffer; // token이 지금 처리해야할 드라이브를 가리킨다.
      while (dwRet > 0)
      {
      DWORD FileSystemFlag;
      char FileSystemName[64];

      strcpy(DriveString, token);
      // VolumeName으로 드라이브에 대한 설명 문자열이 넘어온다.
      if (GetVolumeInformation(token, VolumeName, 255, NULL, NULL,
      &FileSystemFlag, FileSystemName, 63))
      {
      // 원하는 작업을 수행한다.
      }
      dwRet -= (strlen(token)+1);
      token = token + strlen(token)+1; // 다음 드라이브로 진행한다.
      }

    6. 드라이브/디렉토리/파일의 이미지 리스트 인덱스 얻기

    특정 드라이브/디렉토리/파일이 시스템 이미지 리스트에서 어떤 인덱스를 갖는지 알고 싶습니다.

    각 파일이나 드라이브 및 디렉토리에 대한 정보는 Shell 라이브러리에서 제공해주는 SHGetFileInfo 함수를 이용하면 됩니다. 다음의 함수는 첫 번째 인자인 lpFileName으로 주어진 파일에 대한 설명을 두 번째 인자로 받아오고 세 번째 인자로는 시스템 이미지 리스트에서의 인덱스를 얻어옵니다.

    	void GetFileInfo(LPSTR lpFileName, LPSTR lpDesc, int *nIndex)
    {
    DWORD dwAttr;
    SHFILEINFO sfi;

    int hIcon = SHGetFileInfo(lpFileName, dwAttr, &sfi, sizeof(SHFILEINFO),
    SHGFI_TYPENAME | SHGFI_SYSICONINDEX);
    *nIndex = sfi.iIcon;
    strcpy(lpDesc, sfi.szTypeName);
    }

    7. 리스트 컨트롤에 칼럼 헤더 넣기

    리포트뷰 형식의 리스트 컨트롤에 컬럼 헤더를 집어 넣으려면 어떻게 해야합니까 ?

    	// <문서명, 등록날짜, 상태> : 3개의 헤더를 만든다.
    LV_COLUMN col;

    col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    col.fmt = LVCFMT_LEFT;
    col.cx = 100;
    col.pszText = "문서명";
    col.cchTextMax = strlen(col.pszText);
    ListView_SetColumn(hListView, 0, &col);

    col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    col.fmt = LVCFMT_LEFT;
    col.cx = 100;
    col.pszText = "등록날짜";
    col.cchTextMax = strlen(col.pszText);
    ListView_InsertColumn(hListView, 0, &col);

    col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    col.fmt = LVCFMT_LEFT;
    col.cx = 100;
    col.pszText = "상태";
    col.cchTextMax = strlen(col.pszText);
    ListView_InsertColumn(hListView, 1, &col);

    8. 리스트뷰에 항목 삽입하기

    리스트뷰에 한 항목을 추가하고 싶습니다.

    	// 이미지 리스트와 부가 정보를 사용하지 않는 리스트뷰 컨트롤이다.
    int nIndex;
    LV_ITEM item;

    // - 첫번째 컬럼 -
    item.mask = LVIF_TEXT; // 이미지 리스트를 사용하려면 LVIF_IMAGE를 추가하고
    // 부가정보를 지정해야할 일이 있다면 LVIF_PARAM을 추가한다.
    item.pszText = lpDocName;
    item.cchTextMax = strlen(lpDocName);
    item.iItem = 1;
    item.iSubItem = 0;
    nIndex = ListView_InsertItem(hListView, &item);
    // - 두번째 컬럼 -
    item.mask = LVIF_TEXT;
    item.iItem = nIndex;
    item.pszText = lpDate;
    item.cchTextMax = strlen(lpDate);
    item.iSubItem = 1;
    ListView_SetItem(hListView, &item);
    // - 세번째 컬럼 -
    item.mask = LVIF_TEXT;
    item.iItem = nIndex;
    item.pszText = "";
    item.cchTextMax = strlen(lpDocName);
    item.iSubItem = 2;
    ListView_SetItem(hListView, &item);

    9. 리스트뷰 컨트롤에서의 정렬 구현

    리스트뷰 컨트롤에서 칼럼 헤더를 눌렀을 때 정렬이 되도록 하려면 어떻게 해야합니까 ?

    1. 일단 리스트뷰 컨트롤의 생성시 윈도우 스타일로 LVS_NOSORTHEADER를 주지 않는다.
    2. 리스트뷰로부터 칼럼 헤더가 눌렸을 때 오는 이벤트를 받아들인다.
      	NM_LISTVIEW *pnmtv = (NM_LISTVIEW FAR *)lParam;

      switch(pnmtv->hdr.code)
      {
      case LVN_COLUMNCLICK :
      {
      // 어느 항목(pnmtv->iSubItem)이 눌렸는지부터 검사한다.
      // g_iSubItem은 어느 항목이 눌렸는지 기록해두는 인덱스이다.
      g_iSubItem = pnmtv->iSubItem;
      // 정렬함수를 호출한다. CompareFunc가 정렬함수이다.
      ListView_SortItems(hListView, (PFNLVCOMPARE)CompareFunc, (LPARAM)this);
      break;
      }
    3. 리스트뷰 항목을 정렬하는데 사용되는 CompareFunc라는 함수를 만든다. 이는 보통 C 함수로 만들거나 클래스를 사용할 경우에는 클래스 내의 static 함수로 만든다. CompareFunc의 코드는 다음과 같다.
      	int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
      {
      LV_FINDINFO lvfi;
      int iFirstItem, iSecondItem;

      lvfi.flags = LVFI_PARAM;
      lvfi.lParam = lParam1;
      iFirstItem = ListView_FindItem(hListWnd, -1, &lvfi);

      lvfi.flags = LVFI_PARAM;
      lvfi.lParam = lParam2;
      iSecondItem = ListView_FindItem(hListWnd, -1, &lvfi);

      char lpFirst[100];
      char lpSecond[100];
      ListView_GetItemText(hListWnd, iFirstItem, g_iSubItem, lpFisrt, 100);
      ListView_GetItemText(hListWnd, iSecondItem, g_iSubItem, lpSecond, 100);

      // g_iSubItem 컬럼의 성격에 따라 비교한다. 문자열이라면 아래와 같이 한다.
      int iRet = strcmpi(lpFirst, lpSecond);
      return iRet;
      }

    10. 버전 정보 알아내기 코드

    파일의 버전을 API를 통해 알아내려면 어떻게 해야합니까 ?

    Resource의 한 타입으로 VERSIONINFO라는 것이 존재합니다. 여기에 해당 파일의 버전 정보를 기록하도록 되어있습니다. 이 버전 정보를 읽어오는데 ver.dll이라는 DLL에 들어있는 API들을 사용합니다. 주의할 점은 버전 리소스는 언어별로 설정이 되기 때문에 영어로도 읽어보고 한국어 로도 읽어봐야 한다는 것입니다. 다음 예제를 참고하시기 바랍니다.

    	// szDrvName이란 파일에 들어있는 버전 정보를 읽어온다. 
    #include

    DWORD dwSize, handle;
    LPSTR lpstrVffInfo;
    HANDLE hMem;
    LPSTR lpVersion; // 이 변수로 파일의 버전 정보가 들어온다.
    char szDrvName[80]; // 버전 정보를 알아내고자 하는 파일 이름이 여기에 들어온다.

    ....
    // 버전 정보 블록의 크기를 알아온다.
    dwSize = GetFileVersionInfoSize(szDrvName , &handle);
    if (dwSize) // 버전 정보 블록이 존재하면
    {
    // 버전 정보 블록을 포함할 메모리 블록을 할당 받아둔다.
    hMem = GlobalAlloc(GMEM_MOVEABLE, dwSize);
    lpstrVffInfo = GlobalLock(hMem);
    // 버전 정보 블록의 내용을 읽어온다.
    GetFileVersionInfo(szDrvName, handle, dwSize, lpstrVffInfo);
    // 버전 정보 블록에서 버전 정보를 읽어온다.
    VerQueryValue((LPVOID)lpstrVffInfo,
    (LPSTR)"\\StringFileInfo\\041204B0\\FileVersion",
    (void FAR* FAR*)&lpVersion, (UINT FAR *)&dwSize);
    // lpVersion에 들어있는 버전 정보를 사용한다.
    ....
    GlobalUnlock(hMem);
    GlobalFree(hMem);
    }
    위에서 041204B0가 바로 버전 리소스에 사용된 언어가 무엇인지를 나타냅니다. 이는 영어를 나타내며 한국어의 경우에는 040904B0를 사용하면 됩니다. 이 밖에도 version.lib를 링크의 라이브러리 항목에 추가해야 합니다.

    11. 시스템 사양 알아내기

    현재 시스템에 부착되어 있는 메인 메모리의 양과 CPU와 운영체제의 종류를 알고 싶습니다.

    먼저 시스템에 부착되어 있는 메인 메모리의 크기는 GlobalMemoryStatus라는 API를 이용하면 됩니다. 예제 코드는 다음과 같습니다.

    	//===========================================================
    // lMemTotal : 실제 메모리의 전체 크기 (KB 단위)
    // lAvailMemTotal : 사용 가능한 실제 메모리의 크기 (KB 단위)
    // lVirtualTotal : 가상 메모리의 전체 크기 (KB 단위)
    //===========================================================
    void GetMemoryStatus(long *lMemTotal, long *lAvailMemTotal, long *lVirtualTotal)
    {
    double var;
    MEMORYSTATUS memoryStatus;

    memset (&memoryStatus, sizeof (MEMORYSTATUS), 0);
    memoryStatus.dwLength = sizeof (MEMORYSTATUS);

    GlobalMemoryStatus (&memoryStatus);

    lMemTotal = memoryStatus.dwTotalPhys / 1024;
    lAvailMemTotal = memoryStatus.dwAvailPhys / 1024;
    lVirtualTotal = memoryStatus.dwTotalVirtual / 1024;
    }
    다음으로 CPU의 종류를 알아내는 코드는 다음과 같습니다.
    	//===============================================================
    // GetProcessorInfo : 프로세서에 대한 정보를 읽어온다.
    // lpCPUSpeed : CPU의 속도. 기록된 시스템에서만 읽어온다.
    // lpProcessorType : 프로세서의 종류
    // lpNumProcessors : 프로세서의 개수. NT의 경우에만 의미가 있다.
    //===============================================================
    void GetProcessorInfo(LPSTR lpCPUSpeed, LPSTR lpProcessorType, LPSTR lpNumProcessors)
    {
    SYSTEM_INFO sysInfo;
    LONG result;
    HKEY hKey;
    DWORD data;
    DWORD dataSize;

    lpCPUSpeed[0] = 0;
    // ---------------------------------------------
    // 프로세서의 속도를 얻어낸다.
    // ---------------------------------------------
    result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE,
    "Hardware\\Description\\System\\CentralProcessor\\0", 0, KEY_QUERY_VALUE, &hKey);
    if (result == ERROR_SUCCESS)
    {
    result = ::RegQueryValueEx (hKey, "~MHz", NULL, NULL,(LPBYTE)&data, &dataSize);
    wsprintf(lpCPUSpeed, "%d MHz", data);
    }
    RegCloseKey (hKey);

    // ------------------------------------------
    // 하드웨어 정보를 얻어낸다.
    // ------------------------------------------
    GetSystemInfo (&sysInfo);

    // 프로세서 타입부터 검사한다.
    if (sysInfo.dwProcessorType == PROCESSOR_INTEL_386)
    strcpy(lpProcessorType, "Intel 386");
    else if (sysInfo.dwProcessorType == PROCESSOR_INTEL_486)
    strcpy(lpProcessorType, "Intel 486");
    else if (sysInfo.dwProcessorType == PROCESSOR_INTEL_PENTIUM)
    {
    if (sysInfo.wProcessorLevel == 6)
    strcpy(lpProcessorType, "Intel Pentium (II/Pro)");
    else
    strcpy(lpProcessorType, "Intel Pentium");
    }
    else
    strcpy(lpProcessorType, "알 수 없는 시스템");

    // 프로세서의 갯수를 검사한다.
    wsprintf(lpNumProcessors, "%d", sysInfo.dwNumberOfProcessors);
    }

    다음으로 현재 사용 중인 운영체제의 종류를 알아내는 코드는 다음과 같습니다.
    	//===============================================================
    // GetOSVersion : OS의 버전을 얻어온다.
    // --------------------------------------------------------------
    // lpstInfo
    // lpstBuildNumber
    // lpstServicePack
    //===============================================================
    void GetOSVersion (LPSTR lpstInfo, LPSTR lpstBuildNumber, LPSTR lpstServicePack)
    {
    int stat = 0;
    TCHAR data [64];
    DWORD dataSize;
    DWORD win95Info;
    OSVERSIONINFO versionInfo;
    HKEY hKey;
    LONG result;

    lpstServicePack[0] = 0;
    versionInfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);

    // 버전 정보를 얻어낸다.
    if (!::GetVersionEx (&versionInfo))
    {
    strcpy(lpstInfo, "운영체제 정보를 얻을 수 없습니다.");
    return;
    }

    // NT이면 서버인지 웍스테이션인지 검사한다. 이는 레지스트리를 보고 검사한다.
    if (versionInfo.dwPlatformId == VER_PLATFORM_WIN32_NT)
    {
    strcpy(lpstInfo, "Windows NT");
    dataSize = sizeof (data);
    result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE,
    "System\\CurrentControlSet\\Control\\ProductOptions", 0, KEY_QUERY_VALUE, &hKey);
    if (result != ERROR_SUCCESS)
    return;

    result = ::RegQueryValueEx (hKey, "ProductType", NULL, NULL, (LPBYTE) data, &dataSize);
    RegCloseKey (hKey);

    if (result != ERROR_SUCCESS)
    return;

    if (lstrcmpi (data, "WinNT") == 0)
    strcpy(lpstInfo, "Windows NT Workstation");
    else if (lstrcmpi (data, "ServerNT") == 0)
    strcpy(lpstInfo, "Windows NT Server");
    else
    strcpy(lpstInfo, "Windows NT Server - Domain Controller");

    // NT 버전을 알아낸다.
    if (versionInfo.dwMajorVersion == 3 || versionInfo.dwMinorVersion == 51)
    strcat(lpstInfo, " 3.51");
    else if (versionInfo.dwMajorVersion == 5) // 윈도우 2000의 경우
    strcat(lpstInfo, " 5.0");
    else
    strcat(lpstInfo, " 4.0");

    // Build 번호를 알아낸다.
    wsprintf(lpstBuildNumber, "%d", versionInfo.dwBuildNumber);
    }
    else if (versionInfo.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
    {
    strcpy(lpstInfo, "Windows 95");
    if ((versionInfo.dwMajorVersion > 4) || ((versionInfo.dwMajorVersion == 4)
    && (versionInfo.dwMinorVersion > 0)))
    {
    strcpy(lpstInfo, "Windows 98");
    }
    // 윈도우 95는 Build 번호가 하위 워드에 들어간다.
    win95Info = (DWORD)(LOBYTE(LOWORD(versionInfo.dwBuildNumber)));
    wsprintf(lpstBuildNumber, "%d", win95Info);
    }
    else
    wsprintf(lpstInfo, "Windows 3.1");

    // 서비스 팩 정보를 얻어낸다.
    strcpy(lpstServicePack, versionInfo.szCSDVersion);
    }

    12. IE의 설치 여부와 버전 확인

    현재 시스템에 IE가 설치되었는지 여부와 그 버전을 알려면 어떻게 해야합니까 ?

    사실 동작시켜보지 않고서는 IE가 제대로 설치되어있는지 알아내는 방법은 없지만 레지스트리를 통해 IE가 설치되었는지 여부와 버전을 확인할 수 있습니다. 그 함수는 다음과 같습니다.

    	//===========================================================================
    // GetIEVersion : IE의 버전을 얻는다. 정보를 찾을 수 없으면 FALSE를 리턴한다.
    //===========================================================================
    BOOL GetIEVersion(LPSTR lpVer)
    {
    LONG result;
    HKEY hKey;
    DWORD dwType;
    char data[65];
    DWORD dataSize = 64;

    // --------------------
    // IE의 버전을 얻는다.
    // --------------------
    result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Internet Explorer", 0, KEY_QUERY_VALUE, &hKey);
    if (result == ERROR_SUCCESS)
    {
    result = ::RegQueryValueEx (hKey, "Version", NULL, &dwType, (unsigned char *)data, &dataSize);
    strcpy(lpVer, data);
    }
    else
    return FALSE;

    RegCloseKey (hKey);
    return TRUE;
    }

    13. IE의 보안 설정 보기

    IE에 보면 네 단계의 보안 영역이 있습니다. 그 영역별로 설정되어있는 보안 설정값을 읽으려면 어떻게 해야합니까 ?

    IE에는 다음과 같은 네 가지 보안 영역이 존재합니다.

    • 인터넷 영역
    • 로컬 인터넷 영역
    • 신뢰할 수 있는 사이트 영역
    • 제한된 사이트 영역
    IE는 보안 영역 설정과 관련하여 Internet Security Manager와 Internet Zone Manager라는 인터페이스가 존재합니다. 이를 이용해 보안 영역의 보안을 설정하고 특정 IP나 도메인 이름을 등록할 수 있습니다. 자세한 사항은 레퍼런스를 찾아보기 바랍니다.
     
    #include "objbase.h"
    #include "urlmon.h"

    char szTemp1[256];
    char szTemp2[256];
    HRESULT hr;
    IInternetSecurityManager *pSecurityMgr;
    IInternetZoneManager *pZoneMgr;
    DWORD dwEnum, dwZoneCount;
    // --- 변수 선언부
    DWORD dwZone;
    ZONEATTRIBUTES zoneAttr;
    int nLevel = 2;

    // COM 라이브러리를 초기화한다.
    CoInitialize(NULL);
    dwEnum = 0;
    pSecurityMgr = NULL;
    pZoneMgr = NULL;

    // Internet Security 인터페이스 초기화
    hr = CoCreateInstance(CLSID_InternetSecurityManager, NULL, CLSCTX_ALL, //INPROC_SERVER,
    IID_IInternetSecurityManager, (void**)&pSecurityMgr);
    if (hr != S_OK)
    {
    return;
    }
    hr = CoCreateInstance(CLSID_InternetZoneManager, NULL, CLSCTX_ALL, //INPROC_SERVER,
    IID_IInternetZoneManager, (void**)&pZoneMgr);
    if (hr != S_OK)
    {
    return;
    }
    dwEnum = 0;

    // 보안 영역 열거자(Zone Enumerator)를 초기화한다.
    pZoneMgr->CreateZoneEnumerator(&dwEnum, &dwZoneCount, 0);
    for(DWORD i = 1;i < dwZoneCount;i++)
    {
    pZoneMgr->GetZoneAt(dwEnum, i, &dwZone);
    pZoneMgr->GetZoneAttributes(dwZone, &zoneAttr);

    // zoneAttr.szDisplayName에 보안 영역의 이름이 들어오는데 유니코드이다. 이를 변환한다.
    WideCharToMultiByte(CP_ACP, 0, zoneAttr.szDisplayName, -1, szTemp1, 255, NULL, NULL);
    // zoneAttr.dwTemplateCurrentLevel에는 보안 영역의 보안값 설정이 들어온다.
    wsprintf(szTemp2, "%x", zoneAttr.dwTemplateCurrentLevel);
    }

    // 보안 영역 열거자(Zone Enumerator)를 제거한다.
    if (dwEnum != 0)
    pZoneMgr->DestroyZoneEnumerator(dwEnum);

    pSecurityMgr->Release();
    pZoneMgr->Release();
    // COM 라이브러리를 메모리에서 내린다.
    CoUninitialize();
    }

    14. ActiveX 컨트롤의 등록 방법

    Regsvr32 같은 유틸리티를 이용하지 않고 프로그램 내에서 컨트롤을 레지스트리에 등록하려면 어떻게 해야합니까 ?

    모든 AcitveX 컨트롤은 자신을 레지스트리에 등록하기위한 목적으로 DllRegisterServer라는 함수를 갖고 있습니다. ActiveX 컨트롤을 메모리로 로드한 다음에 이 함수를 불러주면 원하는 일을 수행할 수 있습니다. 반대로 ActiveX 컨트롤을 레지스트리에서 제거하기 위한 용도로 DllUnRegisterServer라는 함 수도 존재합니다.

    	// ==============================================================
    // RegisterOCX 지정된 ActiveX 컨트롤을 레지스트리에 등록한다.
    // --------------------------------------------------------------
    // LPSTR pszString 등록하고자 하는 ActiveX 컨트롤의 절대 경로명
    // ==============================================================
    BOOL WINAPI RegisterOCX(LPSTR pszString)
    {
    int iReturn = 0;
    HRESULT (STDAPICALLTYPE * lpDllEntryPoint)();
    HINSTANCE hLib;

    // OLE 라이브러리를 초기화한다.
    if (FAILED(OleInitialize(NULL)))
    {
    MessageBox(GetFocus(), "OLE 초기화 실패", "에러", MB_OK);
    return FALSE;
    }

    // 지정된 activeX 컨트롤을 메모리로 로드한다.
    hLib = LoadLibrary(pszString);
    if (hLib <= NULL)
    {
    MessageBox(GetFocus(), "파일을 로드하는데 실패했습니다.", "에러", MB_OK);
    OleUninitialize();
    return FALSE;
    }

    // "DllRegisterServer" 함수의 위치를 찾는다.
    lpDllEntryPoint = (long (__stdcall *)(void))GetProcAddress(hLib, "DllRegisterServer");

    // 이 함수를 호출합니다.
    if (lpDllEntryPoint)
    {
    if (FAILED((*lpDllEntryPoint)()))
    {
    DWORD dwRet;
    char szTemp[128];

    dwRet = GetLastError();
    wsprintf(szTemp, "에러 번호 : %lx", dwRet);
    MessageBox(GetFocus(), szTemp, "DllRegisterServer 에러", MB_OK);
    FreeLibrary(hLib);
    OleUninitialize();
    return FALSE;
    }
    }
    else
    {
    MessageBox(GetFocus(), "DllRegisterServer를 찾을 수 없습니다.", "에러", MB_OK);
    FreeLibrary(hLib);
    OleUninitialize();
    return FALSE;
    }

    FreeLibrary(hLib);
    OleUninitialize();
    return TRUE;
    }

    15. 데이터 파일의 실행

    탐색기에서 실행 파일이 아닌 데이터 파일을 더블클릭하면 그 데이터 파일과 연결된 실행 파일이 실행되면서 그 파일을 물고 올라갑니다. 이를 구현하는 방법을 알려주세요.

    ShellExecuteEx라는 API를 사용하면 됩니다. 다음 함수는 인자로 데이터 파일 혹은 실행 파일의 경로를 받아서 실행해줍니다. 데이터 파일의 경우에는 연결된 실행 파일을 찾아서 그걸 실행해줍니다.

    	BOOL Execute(LPSTR lpPath)
    {
    char FilePath[255];
    SHELLEXECUTEINFO ExecInfo;

    // lpPath를 나누어 본다.
    char drive[_MAX_DRIVE];
    char dir[_MAX_DIR];
    char fname[_MAX_FNAME];
    char ext[_MAX_EXT];

    _splitpath(lpPath, drive, dir, fname, ext);
    // 디렉토리 경로를 얻는다.
    strcpy(FilePath, drive);
    strcat(FilePath, dir);

    // 파일 이름을 얻는다.
    strcat(fname, ".");
    strcat(fname, ext);

    SHELLEXECUTEINFO ExecInfo;
    ExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
    ExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
    ExecInfo.hwnd = hWnd;
    ExecInfo.lpVerb = "open";
    ExecInfo.lpFile = fname;
    ExecInfo.lpParameters = NULL;
    ExecInfo.lpDirectory = FilePath;
    ExecInfo.nShow = SW_SHOW;
    ExecInfo.hInstApp = g_hInstance; // g_hInstance는 프로그램의 인스턴스 핸들

    return ShellExecuteEx(&ExecInfo);
    }

    16. 파일의 존재 여부 테스트

    어떤 파일이 실제로 존재하는 것인지 간단히 테스트해보는 방법은 무엇인가요 ?

    CreateFile API를 사용하면 됩니다. 파일을 열때 플래그 중의 하나로 OPEN_EXISTING이라는 것이 있는데 이를 사용하면 됩니다. 다음 코드를 예로 보시기 바랍니다.

    	hFile = CreateFile("C:\\TEMP\\test.txt", GENERIC_READ, 0, NULL,  
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (hFile != INVALID_HANDLE_VALUE)
    {
    // 파일이 존재하는 경우
    CloseHandle(hFile);
    }

    17. API를 이용한 파일 I/O

    API를 이용한 파일 입출력에 대한 예제 코드가 없습니까 ?

    프로그래밍을 하다보면 간혹 파일 I/O를 API를 이용해 수행해야 할 경우가 있습니다. 이 때 다음의 예제 코드를 복사해다가 사용하면 편리할 것입니다. MFC의 CFile을 이용한 파일 I/O에 대해 알고 싶으시면 요기를 클릭하세요.

  • 파일 쓰기의 경우
    	HANDLE fp;
    DWORD NumberOfBytesWritten;
    char lpBuffer[1024];

    // FileName이 지정한 파일의 이름이 있으면 그걸 열고 없으면 그 이름으로 하나 생성한다.
    if ((fp=CreateFile((LPCTSTR)FileName, GENERIC_WRITE | GENERIC_READ, 0, NULL,
    OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    {
    // 파일 열기 에러 발생
    }
    // 필요한 만큼 WriteFile의 호출을 반복한다. 파일 포인터의 이동시에는 SetFilePointer API를 이용한다.
    WriteFile(fp, lpBuffer, 1024, &NumberOfBytesWritten, NULL);
    if (NumberOfBytesWritten != 1024)
    {
    // 파일 쓰기 에러 발생
    CloseHandle(fp);
    }
    // 작업이 다 끝났으면 파일을 닫는다.
    CloseHandle(fp);
  • 파일 읽기의 경우
    	HANDLE fp;
    DWORD NumberOfBytesRead;
    char lpBuffer[1024];

    if ((fp=CreateFile((LPCTSTR)FileName, GENERIC_READ,
    FILE_SHARE_READ, NULL, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    {
    // 파일 열기 에러 발생
    }
    // 필요한 만큼 ReadFile의 호출을 반복한다.
    ReadFile(fp, lpBuffer, 1024, &NumberOfBytesRead, NULL);
    if (NumberOfBytesRead != 1024)
    {
    // 파일 읽기 에러 발생
    CloseHandle(fp);
    }
    // 작업이 다 끝났으면 파일을 닫는다.
    CloseHandle(fp);

    파일 포인터의 이동시에는 SetFilePointer라는 API를 사용하고 파일의 크기를 알고 싶을 때는 GetFileSize라는 API를 사용한다.

    18. GetParent API의 리턴값

    다이얼로그 박스에서 GetParent API를 호출했을 때 리턴되는 값이 이상합니다.

    GetParent API의 리턴값은 보통의 윈도우에서는 윈도우 생성시 지정한 부모/자식 윈도우 간의 관계에 따라 달라집니다. 하지만 다이얼로그 박스의 경우에는 다릅니다. 다이얼로그 박스는 GetParent를 호출하면 무조건 그 것이 속한 응용프로그램의 메인 윈도우 핸들이 리턴됩니다.

    19. 특정 프로그램을 실행하고 종료를 기다리기

    특정 프로그램을 실행한 다음에 그 프로그램이 종료될 때 다른 일을 하고 싶습니다. 어떻게 해야합니까 ?

    다음은 CreateProcess를 이용해서 특정 프로그램의 실행이 끝나기를 기다리는 코드입니다.

    	// buffer에 실행하고자하는 파일이름이 들어온다.
    void DoCreate(HWND hWnd, LPSTR buffer)
    {
    STARTUPINFO sui;
    PROCESS_INFORMATION pi;
    DWORD ret;

    memset(&sui, 0x00, sizeof(STARTUPINFO));
    sui.cb = sizeof(STARTUPINFO);

    ret = CreateProcess(buffer, NULL, NULL, NULL, FALSE,
    0, NULL, NULL,&sui, &pi);
    if (ret == TRUE) // 제대로 실행되었으면
    {
    hProcess = pi.hProcess;
    // 실행이 끝나기를 대기한다.
    WaitForSingleObject(hProcess, 0xffffffff);
    CloseHandle(hProcess);
    }
    }

    20. 파일 열기 다이얼로그 띄우기

    API를 이용해 파일 오픈 다이얼로그를 띄우고 싶습니다.

    API를 이용해 파일 오픈 다이얼로그를 띄우는 방법은 다음과 같습니다.

    	#define MAXCHARS   255
    OPENFILENAME ofn;
    char buffer[MAXCHARS];

    memset(&ofn, 0x00, sizeof(OPENFILENAME));
    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = hWnd;
    ofn.lpstrFilter = "모든 파일\000*.*\000\000";
    ofn.lpstrInitialDir = m_szTemp;
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = buffer;
    ofn.nMaxFile = MAXCHARS;
    ofn.lpstrTitle = "파일 선택하기";

    if (GetOpenFileName(&ofn)) // 사용자가 파일을 제대로 선택한 경우
    {
    // ....
    }

    21. 긴 파일 이름과 짧은 파일 이름간의 변환 방법

    주어진 긴 파일 이름에 해당하는 짧은 파일 이름을 알려면 어떻게 해야하나요 ?

  • 긴 파일 이름에서 짧은 파일 이름으로의 변환 : GetShortPathName API 사용
  • 짧은 파일 이름에서 긴 파일 이름으로의 변환 : GetFullPathName API 사용

    22. 시스템 이미지 리스트 얻어내기

    탐색기 등에서 사용되는 시스템 이미지 리스트를 사용할 수 있는 방법이 있는지 알고 싶습니다.

    물론 있습니다. SHGetFileInfo라는 API를 이용하면 됩니다. 이를 이용하면 16X16이나 32X32 크기의 이미지 리스트를 얻어낼 수 있습니다.

    	SHFILEINFO sfi;
    HIMAGELIST sysSmallList, sysLargeList;

    sysSmallList = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &sfi, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
    sysLargeList = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &sfi, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_ICON);

    23. 리스트뷰 컨트롤에서 스타일 동적 변경하기

    리스트뷰 컨트롤에서 리스트(LVS_ICON) 스타일을 사용 중인데 이를 실행 중에 리포트(LVS_REPORT) 스타일로 변경하고 싶습니다. 어떻게 해야할까요 ?

    윈도우의 스타일은 윈도우 엑스트라 바이트라는 영역에 저장됩니다. 이 곳의 데이터를 읽고 쓰는데 GetWindowLong, SetWindowLong 같은 API를 사용하는데 다음 코드를 예로 보시기 바랍니다.

  • 리스트 스타일에서 리포트 스타일로
    	LONG lStyle = GetWindowLong(hListWnd, GWL_STYLE);
    SetWindowLong(hListWnd, GWL_STYLE, (lStyle & ~LVS_LIST) | LVS_REPORT);
  • 리포트 스타일에서 리스트 스타일로
    	LONG lStyle = GetWindowLong(hListWnd, GWL_STYLE);
    SetWindowLong(hListWnd, GWL_STYLE, (lStyle & ~LVS_REPORT) | LVS_LIST);

    24. 윈도우와 다이얼로그에서 클래스 포인터의 사용

    제가 만든 C++ 클래스내에서 윈도우를 생성합니다. 생성한 윈도우의 윈도우 프로시저는 그 클래스의 정적 멤버 함수로 선언되어 있습니다. 정적 함수의 경우에는 데이터 멤버를 접근하지 못하기 때문에 접근하게 하려고 윈도우를 생성한 그 클래스의 객체에 대한 포인터를 전역 변수로 유지하여 사용하고 있습니다. 별로 깨끗한 방법도 아닌 것 같고 또 동시에 여러 개의 객체가 동작할 경우에는 에러가 날 수밖에 없는데 해결 방법이 없을까요 ?

    이는 다이얼로그의 경우에도 마찬가지입니다. 윈도우 생성시 사용하는 CreateWindow나 CreateWindowEx 같은 함수를 보면 마지막 인자로 윈도우 생성 데이터라는 것을 지정하게 되어있습니다. 다이얼로그 생성시에는 DialogBox라는 API말고 DialogBoxParam이라는 API가 있어서 마지막 인자로 초기화 데이터를 넘겨줄 수 있도록 되어있는데 이것과 윈도우 엑스트라 바이트를 같이 사용하면 정적함수로 선언된 윈도우 프로시저나 다이얼로그박스 프로시저내에서 객체에 대한 포인터를 사용할 수 있습니다. 예를 들어 살펴보겠습니다.

  • 윈도우의 경우

    다음과 같이 CExplorerBar라는 클래스내에서 윈도우를 하나 생성합니다. CreateWindowEx 함수나 CreateWindow 함수의 마지막 인자로 this 포인터를 지정합니다.

    	BOOL CExplorerBar::RegisterAndCreateWindow(void)
    {
    ....
    CreateWindowEx( 0, EB_CLASS_NAME, NULL,
    WS_CHILD | WS_CLIPSIBLINGS | WS_BORDER,
    rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
    m_hwndParent, NULL, g_hInst, (LPVOID)this);

    }

    위에서 생성한 윈도우의 윈도우 프로시저는 WndProc이고 CExplorerBar 클래스의 정적 멤버 함수로 존재한다고 가정하겠습니다.

    	LRESULT CALLBACK CExplorerBar::WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
    {
    CExplorerBar *pThis = (CExplorerBar*)GetWindowLong(hWnd, GWL_USERDATA);

    switch (uMessage)
    {
    case WM_NCCREATE:
    {
    LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lParam;
    pThis = (CExplorerBar*)(lpcs->lpCreateParams);
    SetWindowLong(hWnd, GWL_USERDATA, (LONG)pThis);
    }
    break;
    case WM_CREATE :
    return pThis->OnCreate();

    WM_NCCREATE 메시지에서 lParam 인자로 넘어오는 CExplorerBar 객체에 대한 포인터를 받아서 윈도우 엑스트라 바이트로 저장하고 있습니다. 윈도우 엑스트라 바이트는 윈도우 마다 할당되는 고유의 영역으로 사용자 정의 영역으로 GWL_USERDATA가 정의되어 있습니다. 여기에다 CExplorerBar 객체에 대한 포인터를 저장해두고 윈도우 프로시저에 진입할 때마다 이 값을 pThis라는 변수에 대입해 놓고 사용합니다. 참고로 WM_NCCREATE는 WM_CREATE 메시지보다 먼저 발생하는 메시지입니다.

  • 다이얼로그의 경우

    예를 들어 CKTree라는 클래스내의 한 멤버 함수에서 다이얼로그 박스를 띄운다고 가정하겠습니다.

    	BOOL CKTree::SelectFolder(short sTypes, long dwFolderID, short  bCreationEnabled)
    {
    // ......
    if (DialogBoxParam(hInst, MAKEINTRESOURCE(IDD_FOLDER_SELECT), hWnd, (DLGPROC)SelectFolderDlg, (LPARAM)this))

    DialogBoxParam 함수는 DialogBox 함수보다 인자가 하나 더 있는데 그것이 바로 다이얼로그 프로시저의 WM_INITDIALOG 메시지의 lParam 인자로 넘어갑니다. 여기에 CKTree 객체에 대한 포인터를 넘깁니다. 그리고나서 다이얼로그 박스 프로시저의 WM_INITDIALOG 메시지에서 이를 받아서

    	LRESULT CALLBACK SelectFolderDlg(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
    {
    switch (message)
    {
    case WM_INITDIALOG:
    {
    // lParam으로 넘어온 값을 KPointer라는 윈도우 프로퍼티에 저장한다.
    SetProp(hDlg, "KPointer", (HANDLE)lParam);
    .......
    case WM_NOTIFY :
    {
    // CKTree 객체에 대한 포인터가 필요하면 KPointer 윈도우 프로퍼티 값을 읽어들인다.
    CKTree *pKTree = (CKMartTree *)GetProp(hDlg, "KPointer");
    if (pKTree->m_bWait)
    {
    .....

    여기서는 앞서 윈도우와는 달리 윈도우 프로퍼티라는 것을 이용해서 넘어온 포인터 정보를 저장하고 필요할 때 읽어옵니다. 여기서 앞서의 윈도우 엑스트라 바이트를 사용해도 무방합니다.

    25. 특정 프린터로 출력하기

    제 PC에는 두 대의 프린터가 붙어있습니다. 다이얼로그를 띄우지 않고 상황에 따라 다른 프린터로 출력하고 싶은데 동적으로 HDC를 생성하는 방법을 모르겠습니다.

    CreateDC를 이용하면 됩니다. 시스템 디렉토리의 WIN.INI를 보면 [Devices]라는 섹션이 있는데 이 아래로 이 시스템에 설치되어 있는 모든 프린터 드라이버의 목록이 나옵니다. 예를 들어 다음과 같이 나옵니다.

    	[Devices]
    HUNFAX=HUNFAX,FaxModem
    삼성 SLB-6216H PCL5=SSMPCL5,\\영업팀\볼륨프린터
    ......

    프린터별로 DeviceName=DriverName,OutputName와 같은 구조로 구성되어 있습니다. 이 값들을 CreateDC의 인자로 사용하면 됩니다. 예를 들어 위에서 두 번째 프린터의 DC를 생성하려면 다음과 같이 CreateDC를 호출합니다.

    	// hDC = CreateDC(DriverName, DeviceName, OutputName, NULL);
    hDC = CreateDC ("SSMPCL5", "삼성 SLB-6216H PCL5", "\\\\영업팀\\볼륨프린터", NULL) ;

    CreateDC의 마지막 인자로는 프린터의 설정값을 변경할 수 있습니다. 예를 들어 가로로 찍는다든지 2장을 찍는다든지 하는 설정을 변경하는데 사용됩니다. NULL을 주면 디폴트 값을 사용합니다. 다른 설정을 사용하고 싶다면 다음과 같이 합니다. CreateDC 호출 앞에서 DocumentProperties라는 함수를 호출하여 프린터의 기본 설정을 읽어온 다음에 이를 변경합니다. 다음 예는 출력 방향을 가로로 변경하는 예제입니다.

    	// 프린터의 디폴트 설정을 읽어온다.
    LPDEVMODE lpoutDevMode;
    HANDLE hPrinter;
    HDC hPrnDC;

    // 프린터의 핸들을 얻는다.
    if (OpenPrinter( lpDeviceName, &hPrinter, NULL))
    {
    // OpenPrinter로 얻은 프린터의 초기 설정을 DocumentProperties API로 얻어온다.
    // 먼저 마지막 인자를 0으로 해서 DocumentProperties를 호출하여 필요한 버퍼의 크기를 알아옵니다.
    long lSize = DocumentProperties(GetFocus(), hPrinter, lpPrinterName, NULL, NULL, 0);
    lpoutDevMode = (LPDEVMODE)malloc(lSize);
    long lRet = DocumentProperties(GetFocus(), hPrinter, lpPrinterName, lpoutDevMode, NULL, DM_OUT_BUFFER);
    if (lRet == IDOK)
    {
    // 프린터의 인쇄 방향 설정을 변경한다.
    // 여기서 원하는 변환을 수행한다.
    lpoutDevMode->dmOrientation = DMORIENT_LANDSCAPE;
    }
    hPrnDC = CreateDC (lpDriverName, lpDeviceName, lpOutputName, lpoutDevMode) ;
    free(lpoutDevMode);
    return hPrnDC;
    }

    26. 메뉴 관련 함수

    메뉴 항목을 하나 추가하려고 합니다. InsertMenuItem API를 사용하는데 윈도우 3.1에서와 사용법이 다른 것 같습니다.

    사용법이 달라졌습니다.

    	MENUITEMINFO mii;

    memset(&mii, 0x00, sizeof(MENUITEMINFO));
    mii.cbSize = sizeof(MENUITEMINFO);
    mii.fMask = MIIM_TYPE;
    mii.fType = MFT_SEPARATOR;
    InsertMenuItem(hSubMenu, GetMenuItemCount(hSubMenu), TRUE, &mii);

    mii.cbSize = sizeof(MENUITEMINFO);
    mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID;
    mii.fType = MFT_STRING;
    mii.fState = MFS_DEFAULT | MFS_UNHILITE;
    mii.wID = ID_WORKPLACE_REMOVE;
    mii.dwTypeData = "바구니에서 제거";
    InsertMenuItem(hSubMenu, GetMenuItemCount(hSubMenu), TRUE, &mii);

    27. 코드 실행 중에 다른 윈도우 메시지 처리하기

    하나의 함수 내에서 시간이 오래 걸리는 작업을 하고 있습니다. 이 때 작업 취소를 위한 별도의 다이얼로그를 하나 띄워 두었는데 이 쪽의 버튼이 눌리지 않습니다. 어떻게 해야할까요 ?

    시간이 오래 걸리는 작업을 별도의 스레드로 만들어 처리하던지 아니면 시간이 오래 걸리는 작업을 수행하는 함수 안에서 다음 코드를 가끔 호출해주면 됩니다. 만일 루프를 돌고 있다면 루프내에서 한번씩 호출해주면 됩니다.

    	MSG       msg;

    while (PeekMessage(&msg, NULL, NULL, NULL, TRUE))
    {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
    }

    VB에서라면 DoEvents라는 메소드를 호출해주면 됩니다.

    28. 메인 윈도우에서 캡션을 제거하고 싶습니다. 어떻게 해야 합니까 ?

    윈도우를 생성할 때 WS_CAPTION이란 스타일을 지정하지 않아도 항상 윈도우의 캡션이 보입니다. 이를 제거하려면 어떻게 해야 하나요 ?

    캡션을 제거하려는 윈도우의 WM_NCCREATE 메시지를 처리해야 합니다. 이 메시지는 WM_CREATE 메시지보다 앞서 발생하는 메시지입니다. NC는 Non-Client를 나타냅니다. GetWindowLong과 SetWindowLong API를 이용해서 WS_CAPTION 스타일을 제거합니다. 이 두 API는 앞서 리스트뷰 컨트롤에서 스타일 동적 변경하기에서 이미 사용해본 바 있습니다.

    		case WM_NCCREATE :
    {
    long lStyle;

    lStyle = GetWindowLong(hWnd, GWL_STYLE);
    lStyle = (lStyle & (~WS_CAPTION));
    SetWindowLong (hWnd, GWL_STYLE, lStyle);
    return TRUE;
    }

    29. 사각형 형태 이외의 모양을 갖는 윈도우를 띄우고 싶습니다.

    윈도우는 기본 모양이 사각형인데 다른 형태의 모양을 갖는 윈도우를 띄우려면 어떻게 해야합니까 ?

    원하는 모양을 Region이란 것으로 만들어야 합니다. 만든 다음에 이것을 원하는 시점에 SetWindowRgn라는 API를 이용해 윈도우에 설정해주면 됩니다. 예를 들어 타원 모양의 윈도우를 띄우고 싶다면 다음과 같이 해주면 됩니다.

    	HRGN g_hRgn;

    g_hRgn = CreateEllipticRgn(0, 0, 700, 600);
    SetWindowRgn(hWnd, g_hRgn, FALSE);

    이렇게 했을 경우 윈도우의 위치 이동이 문제가 됩니다. 보통 캡션을 잡고 이동시켜야 하는데 캡션이 없으니까 문제가 됩니다. 이에 관한 것은 31. 윈도우의 이동 처리하기를 참고하기 바랍니다.

    30. 시스템에 설치되어 있는 모든 프린터 드라이버 알아내기

    현재 시스템에 설치되어 있는 모든 프린터 드라이버의 종류를 알아내고 싶습니다.

    EnumPrinters라는 API를 사용하면 됩니다. 다음 코드는 현재 시스템에 설치되어 있는 모든 프린터 드라이버(로컬과 네트웍 프린터 포함)의 이름을 메시지박스로 보여주는 예제입니다.

    	BOOL bSuccess;
    DWORD cbRequired, cbBuffer, nEntries;
    PRINTER_INFO_1 *lpBuffer = NULL;

    // 버퍼의 크기를 알아낸다. cbRequired로 들어온다.
    EnumPrinters(PRINTER_ENUM_CONNECTIONS | PRINTER_ENUM_LOCAL, NULL, 1, (unsigned char *)lpBuffer, 0, &cbRequired, &nEntries);
    cbBuffer = cbRequired;
    // 버퍼를 다시 버퍼를 잡는다.
    lpBuffer = (PRINTER_INFO_1 *)malloc(cbBuffer);
    // 프린터의 종류를 알아낸다.
    bSuccess = EnumPrinters(PRINTER_ENUM_CONNECTIONS | PRINTER_ENUM_LOCAL, NULL, 1, (unsigned char *)lpBuffer, cbRequired, &cbRequired, &nEntries);
    if (bSuccess == FALSE)
    {
    free(lpBuffer);
    // 다른 이유로 에러가 난 경우
    return;
    }
    // 알아낸 프린터를 하나씩 enumerate한다.
    for (int i = 0;i < nEntries; i++)
    {
    ::MessageBox(NULL, lpBuffer[i].pName, "프린터 이름", MB_OK);
    }
    free(lpBuffer);

    31. 윈도우의 이동 처리하기

    보통 윈도우는 캡션 영역을 잡고 위치 이동을 수행하게 되는데 윈도우의 특정 영역을 잡고 이동할 수 있게 하려면 어떻게 해야합니까 ?

    WM_NCHITTEST라는 메시지를 잘(?) 처리하면 어느 영역이든 윈도우 이동을 처리할 수 있습니다. 마우스로 윈도우 위를 이동하면 WM_NCHITTEST, WM_SETCURSOR, WM_MOUSEMOVE 같은 메시지들이 발생합니다. WM_NCHITTEST는 현재 마우스가 윈도우의 어느 영역위에 있는지 알아내기 위해 사용됩니다. 이 메시지의 처리부에서 HTCAPTION이란 값을 리턴해주면 윈도우 운영체제는 지금 마우스 포인터가 윈도우의 캡션 부분에 와있다고 생각해서 여기서 드래그 작업이 시작될 경우에 윈도우의 위치를 이동시켜 버립니다. 다음 코드는 WM_NCHITTEST 메시지를 처리하여 현재 마우스 좌표가 정해진 영역에 있으면 HTCAPTION을 돌려주는 예제입니다.

    	case WM_NCHITTEST:
    {
    // 현재 마우스 위치를 바탕으로 pt 변수를 채운다.
    POINT pt(LOWORD(lParam), HIWORD(lParam));

    // 마우스 좌표를 윈도우의 좌측 상단 기준의 좌표로 변경한다.
    ScreenToClient(hWnd, &pt);
    // 지정된 사각형 안에 포함되는 점인지 검사한다.
    // g_TitleRect는 RECT 타입의 변수로 지정된 사각형의 좌표가 들어있다.
    if (PtInRect(&g_TitleRect, pt))
    return HTCAPTION;
    break;
    }

    32. 바탕 화면 위의 모든 윈도우를 최소화하거나 모든 최소화 실행 취소

    바탕 화면 위의 모든 윈도우를 최소화하거나 모두 최소화 실행 취소를 프로그램으로 구현하는 방법을 알고 싶습니다.

    태스크바의 빈 공간을 오른쪽 마우스 버튼으로 클릭해보면 팝업 메뉴가 뜨는데 거기에 보면 "모든 창을 최소화(M)"와 "모두 최소화 실행 취소(U)" 명령이 존재하는데 그것을 대신 선택해주는 형식으로 프로그램을 작성해주면 됩니다.

    다음은 "모든 창을 최소화"해주는 루틴입니다. keybd_event API를 이용해서 사용자가 키입력한 것처럼 흉내내줍니다.

    	void IconizeAllWindow()
    {
    keybd_event(0x5b, 0, 0, 0);
    keybd_event(77, 0, 0, 0); // 'M' key
    keybd_event(0x5b, 0, 2, 0);
    }

    다음은 "모두 최소화 실행 취소" 루틴입니다.

    	void RestoreWindowState()
    {
    keybd_event(0x5b, 0, 0, 0);
    keybd_event(84, 0, 0, 0); // 'U' key
    keybd_event(0x5b, 0, 2, 0);
    }

    33. VxD 드라이버 호출하기

    Vxd 드라이버를 동적으로 로드해서 호출하고 싶습니다. 어떻게 해야합니까 ?

    먼저 해당하는 VxD 드라이버의 이름과 위치와 호출하려는 작업의 작업 코드명을 알아야 합니다. 드라이버의 로드는 CreateFile API를 이용합니다. VxD 드라이버의 호출은 DeviceIoControl API를 이용합니다. 자세한 설명은 DeviceIoControl API의 레퍼런스를 참고하기 바랍니다.

    	DWORD byteReturned;

    // 먼저 VxD 드라이버를 오픈한다.
    hDevice = CreateFile("\\\\.\\NMOUSE.VXD", 0, 0, 0, CREATE_NEW, FILE_FLAG_DELETE_ON_CLOSE, 0);
    if (DeviceIoControl(hDevice, W32_SETHWND, &hWnd, 4, NULL, 0, &byteReturned, NULL))
    {
    // Success !!!
    }

    Vxd 드라이버는 kernel 모드(이를 윈도우에서는 Ring 0라고 부릅니다)에서 동작하기 때문에 모든 하드웨어와 메모리를 바로 접근할 수 있습니다. VxD 드라이버를 작성하려면 DDK를 이용하거나 Numega의 DriverStudio나 KRFTech사의 WinDriver를 이용해야 합니다.

    34. Thread 실행시 에러

    CreateThread를 이용해 스레드를 만들어 생성하고 있습니다. 루틴에 이상은 없는 것 같은데 스레드가 많이 생성되어 시간이 좀 지나면 에러가 발생합니다. 이유가 무엇일까요 ?

    정말로 스레드 코드에 별 이상이 없다면 CreateThread API 대신에 beginthread나 beginthreadex를 사용해보기 바랍니다. 자세한 사항은 마이크로소프트의 Knowledge base를 참고하시기 바랍니다.

    35. 윈도우 운영체제 종료하기

    프로그램에서 특정 상황이 되면 윈도우 운영체제를 종료하고 싶습니다.

    ExitWindowsEx API를 사용하면 됩니다. 이 함수의 원형은 다음과 같습니다.

    	BOOL ExitWindowsEx(UINT uFlags, DWORD dwReserved);

    uFlags로 종료방법을 지정할 수 있습니다. 다음과 같은 값이 가능합니다.

    EWX_LOGOFF 현재 사용자를 로그오프한다.
    EWX_POWEROFF 시스템을 종료하고 파워오프한다. 파워오프는 이를 지원하는 하드웨어에서만 가능하다.
    EWX_REBOOT 시스템을 종료하고 시스템을 재시동한다.
    EWX_SHUTDOWN 시스템을 종료한다.
    EWX_FORCE WM_QUERYSESSION이나 WM_ENDQUERYSESSION을 보내지 않고 실행중인 모든 프로세스를 종료한다. 위의 네 가지 플래그들과 함께 사용할 수 있다.

    36. 디폴트 웹 브라우저 알아내기

    디폴트로 지정된 웹 브라우저를 실행하는 방법을 알고 싶습니다.

    디폴트로 지정된 웹 브라우저는 레지스트리에 자신을 등록합니다. .htm (혹은 .html) 파일의 편집기로 링크도 되지만 http, ftp, gopher 등의 프로토콜 연결 프로그램으로 등록됩니다. 제 생각에 가장 좋은 것은 http 프로토콜의 연결 프로그램을 찾아보는 것으로 생각됩니다. 다음 레지스트리 항목에 보면 연결된 웹 브라우저의 절대 경로를 알 수 있습니다.

        HKEY_CLASSES_ROOT\http\shell\open\command

    이 항목의 값을 읽는 방법은 3. 레지스트리 읽기/쓰기를 참고하고 프로그램의 실행에 관한 부분은 19. 특정 프로그램을 실행하고 종료를 기다리기를 참고하거나 ShellExecute API 혹은 WinExec API를 사용하면 됩니다. 이 기능을 수행하는 함수는 다음과 같습니다.

    void LaunchDefaultWebBrowser(HWND hWnd)
    {
    // HKEY_CLASSES_ROOT\http\shell\open\command
    DWORD dwType, cbData;
    HKEY hSubKey;
    long lRet;
    LPSTR pszString, pszSrcPath;

    // 키를 오픈한다.
    if ((lRet = RegOpenKeyEx(HKEY_CLASSES_ROOT, "http\\shell\\open\\command",
    0, KEY_READ | KEY_QUERY_VALUE , &hSubKey)) == ERROR_SUCCESS)
    {
    cbData = 255; // 문자열 값을 읽어올 데이터의 크기를 준다.
    pszString = (LPSTR)malloc(255);
    pszSrcPath = pszString;
    if ((lRet = RegQueryValueEx(hSubKey, "",
    NULL, &dwType, (unsigned char *)pszString, &cbData)) == ERROR_SUCCESS)
    {
    // pszString에 디폴트 웹 브라우저의 경로가 들어온다.
    // pszString에서 "를 제거한다.
    RemoveChar(pszString, '"');
    WinExec(pszString, SW_SHOWNORMAL);
    }
    free(pszString);
    RegCloseKey(hSubKey);
    }
    }

    void RemoveChar(LPSTR lpSrc, char chRemove)
    {
    LPTSTR pstrSource = lpSrc;
    LPTSTR pstrDest = lpSrc;
    LPTSTR pstrEnd = lpSrc + strlen(lpSrc);

    while (pstrSource < pstrEnd)
    {
    if (*pstrSource != chRemove)
    {
    *pstrDest = *pstrSource;
    pstrDest = _tcsinc(pstrDest);
    }
    pstrSource = _tcsinc(pstrSource);
    }
    *pstrDest = '\0';
    }

    Copyright 1999© 한기용 Last updated: 02/22/2007 18:46:09 Designed By 한기남

    37. 윈도우의 최대/최소 크기 설정

    윈도우의 캡션을 없앴을 경우 윈도우를 최대화했을 때 아래의 Task Bar가 가려져 버리는 현상이 생기는데.. 캡션이 있으면 Task Bar 위로만 최대화되는데 말입니다. 어떻게 해결할 수 있는 방법이 없나 궁금하네요..

    말씀하신 문제를 해결하려면 WM_GETMINMAXINFO 메시지를 처리해야 합니다. 이는 윈도우의 최대 크기 등을 설정하기 위해 사용되는 메시지입니다. 다음과 같이 처리합니다.

    case WM_GETMINMAXINFO :
    {
    LPMINMAXINFO lpmmi;
    RECT rc;

    SystemParametersInfo(SPI_GETWORKAREA, 0, &rc,0);
    lpmmi = (LPMINMAXINFO)lParam;
    lpmmi->ptMaxSize.x = rc.right;
    lpmmi->ptMaxSize.y = rc.bottom;
    lpmmi->ptMaxPosition.x = 0;
    lpmmi->ptMaxPosition.y = 0;
    lpmmi->ptMinTrackSize.x = GetSystemMetrics(SM_CXMINTRACK);
    lpmmi->ptMinTrackSize.y = GetSystemMetrics(SM_CYMINTRACK);
    lpmmi->ptMaxTrackSize.x = GetSystemMetrics(SM_CXMAXTRACK);
    lpmmi->ptMaxTrackSize.y = GetSystemMetrics(SM_CYMAXTRACK);
    break;
    }

    38. Thread에서 Automation 메소드 호출시 에러 발생

    Thread를 생성하고 Automation 메소드를 호출했는데 에러가 발생합니다.

    App 클래스의 InitInstance 함수에서 AfxOleInit를 호출하는 부분을 CoInitializeEx(NULL, COINIT_MULTITHREADED)를 호출하는 것으로 변경하기 바랍니다. 그리고 App 클래스에 ExitInstance 함수를 추가하고 거기서 CoUninitialize를 호출하도록 하면 됩니다. MFC의 AfxOleInit는 기본적으로 STA(Single Threading Apartment) 모델을 사용합니다. Thread에서 자신이 생성하지 않는 COM 객체를 접근할 때는 MTA(Multiple Threading Apartment) 모델을 사용해야 합니다.

    39. 최상위 윈도우의 종료 방법

    현재 최상위 윈도우를 찾아서 종료하는 코드를 만들고 싶습니다.

    일단 현재 사용자가 작업 중인 최상위 윈도우의 핸들은 GetForegroundWindow API로 얻어냅니다. 그런데 그 윈도우가 자식 윈도우일 수 있기 때문에 GetParent API를 반복적으로 사용해서 최상위 탑 레벨 윈도우의 핸들을 알아냅니다. 종료하는 방법은 먼저 DestroyWindow를 호출해서 시도해보고 실패하면 시스템 메뉴의 "닫기" 명령을 이용해 처리합니다. 사실 이 것도 실패할 수 있는데 무조건 종료시키고 싶다면 아래 코드에서 주석 처리해 놓은 GetWindowThreadProcessId/Terminate API 부분을 사용하면 됩니다.

        HWND hTopWnd = GetForegroundWindow();
    if (hTopWnd == NULL)
    {
    return;
    }

    while(GetParent(hTopWnd))
    {
    hTopWnd = GetParent(hTopWnd);
    }

    if (DestroyWindow(hTopWnd) == FALSE)
    {
    SendMessage(hTopWnd, WM_SYSCOMMAND, SC_CLOSE, NULL);
    //GetWindowThreadProcessId(hTopWnd, &dwProcessId);
    //TerminateProcess(dwProcessId);
    }

    40. 인터넷 익스플로러의 위치 경로 알아내기

    인터넷 익스플로러가 설치된 절대 경로를 알고 싶습니다.

    레지스트리의 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths\IExplore.exe 의 기본 값으로 인터넷 익스플로러의 설치 경로가 들어옵니다. 다음 함수를 호출하면 설치 경로를 얻어 줍니다. 인자로 넘어가는 lpPath는 적어도 256바이트 이상의 크기를 갖는 문자 배열이어야 합니다.

    BOOL GetIEPath(LPTSTR lpPath) 
    {
    long lRet;
    HKEY hKey;

    lRet = RegOpenKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\IExplore.exe", &hKey);
    if (lRet == ERROR_SUCCESS)
    {
    long cbData = 255;

    RegQueryValue(hKey, "", lpPath, &cbData);
    RegCloseKey(hKey);
    }
    else
    return FALSE;
    return TRUE;
    }
  • 2007/03/20 16:35 2007/03/20 16:35
    Posted
    Filed under About Knowledge/Programs_C++

    아래의 글은 인터넷의 어떤 게시판으로 부터 얻어온 글이다.

    참 편리하기도 하지 ..인터넷... 

    =======================================================================================================

    우선 클래스 멤버 함수를 윈도우 프로시져로 등록하는건 간단합니다.
    아래처럼 클래스 선언에서 함수명 앞에 static를 붙여주기면 하면 됩니다.

    class A
    {
    public:
        HWND CreateEventWnd(HINSTANCE hInstance);
        static LRESULT CALLBACK EventWndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
             LPARAM lParam);
    }

    멤버 함수들은 보이지 않게 인자 하나를 추가로 더 전달받게 되는데요.
    아시겠지만 this 포인터 입니다.
    이 this 포인터가 인자로 하나 더 붙기 때문에 윈도우 프로시져가 될 수 없습니다.
    그래서 this 포인터를 전달하기 않게 하기 위해 static 멤버로 만듭니다.  
    이렇게 하면 컴파일 에러 없이 잘 동작 합니다.

    하지만 문제는 this 포인터를 전달받지 못하기 때문에
    클래스의 다른 멤버들을 접근 할 수 없게 됩니다.
    어떤 다른 방법을 통하여 this 포인터를 전해 줘야 합니다.
    아래는 m_szMessage 멤버 변수에 들어있는 메시지를
    윈도우 중앙에 뿌려주는 간단한 예제 입니다.

    class A
    {
    public:
        HWND CreateEventWnd(HINSTANCE hInstance);
        static LRESULT CALLBACK EventWndProc(HWND hWnd, UINT nMsg, WPARAM wParam,
            LPARAM lParam);
    private:
        char m_szMessage[256]; // 여기에 들어가는 메시지를 화면에 뿌려준다.
    };

    HWND A::CreateEventWnd(HINSTANCE hInstance)
    {
        // 메시지를 준비한다.
        // 이 메시지가 윈도우 중앙에 뿌려진다.
        strcpy(m_szMessage, "Hello, world!");

        WNDCLASSEX wndclass;
        wndclass.lpfnWndProc = EventWndProc;
        ......

        // 윈도우를 생성할 때 this 포인터를 넘겨준다.
        return CreateWindowEx(/*앞 부분 인자 생략*/, this);
    }

    LRESULT CALLBACK A::EventWndProc(HWND hWnd, UINT nMsg, WPARAM wParam,
        LPARAM lParam)
    {
        A *pA; // this를 대신할 포인터 변수

        switch(nMsg)
        {
        case WM_CREATE:
            // 생성될 때 넘겨받은 this 포인터를 보관한다.
            pA = *(A **) lParam;
            SetWindowLong(hWnd, GWL_USERDATA, (LONG) pA);
            return 0;  
        case WM_PAINT:
            hDC = BeginPaint(hWnd, &ps);
            GetClientRect(hWnd, &rect);

            // this 포인터를 얻어온다.
            pA = (A *) GetWindowLong(hWnd, GWL_USERDATA);

            // 멤버에 접근 할 때는 pA-> 를 사용해서 접근한다.
            // 그냥 m_szMessage만 달랑 쓰면 컴파일 에러가 날것이다.
            DrawText(hDC, pA->m_szMessage, -1, &rect,
                DT_SINGLELINE | DT_CENTER | DT_VCENTER);

            EndPaint(hWnd, &ps);
            return 0;
        }  
        return DefWindowProc(hWnd, nMsg, wParam, lParam);
    }


    위에것은 구식의 방법이다..


    요즘의 밑에 방법으로  -0-/

    ------------------------------------------------------------------------------

    class A
    {
    public:
        HWND CreateEventWnd(HINSTANCE hInstance);
        static LRESULT CALLBACK EventWndProc(HWND hWnd, UINT nMsg, WPARAM wParam,
            LPARAM lParam);
    private:
        char m_szMessage[256]; // 여기에 들어가는 메시지를 화면에 뿌려준다.
    };

    HWND A::CreateEventWnd(HINSTANCE hInstance)
    {
        // 메시지를 준비한다.
        // 이 메시지가 윈도우 중앙에 뿌려진다.
        strcpy(m_szMessage, "Hello, world!");

        WNDCLASSEX wndclass;
        wndclass.lpfnWndProc = EventWndProc;
        ......

        // 윈도우를 생성할 때 this 포인터를 넘겨준다.
        return CreateWindowEx(/*앞 부분 인자 생략*/, this);
    }

    LRESULT CALLBACK A::EventWndProc(HWND hWnd, UINT nMsg, WPARAM wParam,
        LPARAM lParam)
    {
        static A *pA; // this를 대신할 포인터 변수

        switch(nMsg)
        {
        case WM_CREATE:
            // 생성될 때 넘겨받은 this 포인터를 보관한다.
            pA = (A *)(((LPCREATESTRUCT)lParam)->lpCreateParams);
            return 0;  

        case WM_PAINT:
            hDC = BeginPaint(hWnd, &ps);
            GetClientRect(hWnd, &rect);

            // 멤버에 접근 할 때는 pA-> 를 사용해서 접근한다.
            // 그냥 m_szMessage만 달랑 쓰면 컴파일 에러가 날것이다.
            DrawText(hDC, pA->m_szMessage, -1, &rect,
                DT_SINGLELINE | DT_CENTER | DT_VCENTER);

            EndPaint(hWnd, &ps);
            return 0;
        }  
        return DefWindowProc(hWnd, nMsg, wParam, lParam);
    }

    음 괜찮은 팁이다.. ㅋㅋ  예전에 알았더면 나름대로 잘 써먹었을텐데...

    2007/03/15 19:33 2007/03/15 19:33
    Posted
    Filed under About Knowledge/Programs_C++

    클래스 맴버로 서브클래싱을 구연하기 위해 찾았던 글 제일 많이 검색된 내용은 아래와같았으나,
    글이 약간 모호하고, 원작자도 누군지 알수가 없었다. 감사한 마음으로 무식을 탈피 하는데 아주 유용하겠노라 한다.

    ============================================================================================


    클래스 맴버함수를 콜백함수로 사용하기

    (passing class member function as callback function)



    ---------------------------------------------------

    이 문제는 윈도우즈 API 와 C++ 언어를 같이 사용할 때 부딪히는 문제이다.

    콜백함수를 인자로 요구하는 API 에 클래스의 멤버함수를 넘겨 줄 수는 없는 것인가?

    여기 그 해결의 가능성을 보여준다.

    ---------------------------------------------------



    항상 고민해 왔던것.

    마음속 깊은곳으로 부터, 나를 괴롭히던 문제.

    DLGPROC 형 파라메터에 클래스의 맴버함수를 넘겨줄수는 없는것인가.




    DialogBoxParam( hInst,

    MAKEINTRESOURCE(IDD_MAIN),NULL,(DLGPROC)CMyApp::DlgProcMain,(LPARAM)&DlgClass );


    INT_PTR DialogBoxParam(

    HINSTANCE hInstance,     // handle to module

    LPCTSTR lpTemplateName,  // dialog box template

    HWND hWndParent,         // handle to owner window

    DLGPROC lpDialogFunc,    // dialog box procedure

    LPARAM dwInitParam       // initialization value

    );



    typedef INT_PTR (CALLBACK* DLGPROC)(HWND, UINT, WPARAM, LPARAM);






    C 로 작성된 API 에 C++ 클래스를 접목시킬때 부딧히는 문제이다.

    application 의 기능을 하나의 클래스에 넣고, 그 클래스의 멤버함수중 하나를 message procedure 함수로 만드려는 시도는 제정


    신을 가진 프로그래머의 인지상정이다.

    해봐서 알겠지만 메시지프로시저 함수의 포인터를 받는 함수에 클래스의 멤버함수를 넘겨주면 다음과 같은 에러가 발생한다.



    : error C2440: 'type cast' : cannot convert from '' to 'int (__stdcall *)(struct HWND__

    *,unsigned int,unsigned int,long)'

           None of the functions with this name in scope match the target type



    리턴타입, 파라메터리스트가 완전히 일치하는 멤버함수에서 발생하는 위와 같은 에러메시지는 프로그래머를 좌절시키기에 충분하


    다.

    멤버함수라는게 클래스의 인스턴스가 존재해야 호출할 수 있음을 알고는 있지만 위와 같은 코드를 쓰고 싶은것이다.

    그래서, 클래스의 인스턴스가 없어도 호출할 수 있는 멤버함수를 찾게된다. 그래서 찾은 것이 static 멤버함수. 이것이다.

    심장이 두근 거린다. 드디어 매끈한 코드를 만들수 있는것이다.


    멤버함수를 static 으로 선언한다.


    static LRESULT CALLBACK DlgProcMain (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);



    이상과 실제는 언제나 다르지만, 프로그래밍에서는 그 정도가 더욱 처절하다.

    메시지 프로시저 기능을 하는 멤버함수를 다음과 같이 작성하였다.



    LRESULT CALLBACK CMyApp::DlgProcMain(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

    {    

       switch(uMsg)                                                    

       {

           case WM_INITDIALOG:

           return OnInitDialog( hwnd );

       }

       return FALSE;

    }



    위에서 OnInitDialog 함수는 분명 클래스의 멤버함수이다.

    멤버함수에서 멤버함수를 호출하는것은 당연한 일이고, 언제나 권장되는 일이고, 꼭필요한 일이기도 하다.

    그러나, 컴파일러는 아직도 뭔가 할말이 있다.



    : error C2352: 'CTTSApp::OnInitDialog' : illegal call of non-static member function

    : see declaration of 'OnInitDialog'



    static 함수에서 non-static member 함수를 호출할 수 없다는 얘긴데,

    여기서 프로그래머는 속으로 중얼거린다. "역시 안되는구나."

    문제는 OnInitDialog 이다.

    static 함수에서 호출가능한 함수의 적법한 형태는 이렇다.



    <1> static 함수내에서 static 함수를 호출하는것은 적법하다.

    <2> static 함수내에서 객체의 인스턴스를 통한 멤버함수 호출은 적법하다.



    <1> 의 방법을 사용하면 클래스의 모든 멤버를 static 으로 만들어야 한다. 물론 이것은 프로그래머가 의도한 바는 아니다.

    <2> 방식으로 할경우, 머리에 먼저떠오르는 한줄의 코드는 바로 이것이다.



    this->OnInitDialog( hwnd );



    흐믓한 미소를 지으며 멋지게 한줄을 수정하고 컴파일을 하면

    다음과 같은 컴파일러의 답변을 들을 수 있다.



    : error C2671: 'DlgProcMain' : static member functions do not have 'this' pointers



    static 멤버 함수는 this 포인터가 없다는 얘긴데.

    첩첩산중, 사방이 막혀있는 곳에서 분투하는 프로그래머는 오늘도 고달프다못해 외롭기까지하다.

    <2> 방식으로 할 경우, 또다시 머리에 떠오르는 한줄의 코드는 이것이다.



      CMyApp* pThis = 0;

       switch(uMsg)                                                    

       {

           case WM_INITDIALOG:

               return pThis->OnInitDialog( hwnd );

       }




    컴파일이 된다. 멋지다.

    물론 실행되지는 않는다. 그러나 컴파일이 된다는 사실이 중요하다.

    실행이 되게 하려면



      CMyApp* pThis = 0;



    에서 변수 pThis 에 뭔가 의미있는 값을 넣어주면 실행이 된다.

    바로 생성된 CMyApp 객체의 주소를 넣어주면 된다.

    변수 pThis 에 의미있는 값을 넣어주는 방법은 여러가지가 있을것이다.

    프로그래머 마음대로 하면될것이다.

    여기서는 그중에서 한가지 예를 보인다.



    CTTSApp DlgClass( hInst ); // 객체를 생성한다.


    // 생성된 객체의 주소를 넘겨준다.

    DialogBoxParam( hInst,

    MAKEINTRESOURCE(IDD_MAIN),NULL,(DLGPROC)CMyApp::DlgProcMain,(LPARAM)&DlgClass );


    // WM_INITDIALOG 메시지에서 lParam 인자를 통해 전달된 주소를 받는다.

    LRESULT CALLBACK CMyApp::DlgProcMain(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

    {

       CTTSApp* pThis = (CTTSApp *)::GetWindowLong( hwnd, GWL_USERDATA );

       

       switch(uMsg)                                                    

       {

           case WM_INITDIALOG:

               ::SetWindowLong( hwnd, GWL_USERDATA, lParam );

               pThis = (CTTSApp *)lParam;

               return pThis->OnInitDialog( hwnd );  




           case WM_HSCROLL:

               pThis->HandleScroll( (HWND) lParam );

              return TRUE;

       }

       return FALSE;

    }



    위에서 사용한것은

    DialogBoxParam 함수의 dwInitParam 인자가

    WM_INITDIALOG 메시지의 lParam 으로 전달된다는 사실이다.


    WM_INITDIALOG 메시지에서 lParam 으로 전달된 객체의 포인터를 저장하는 모습을 볼수있다.

    다른 메시지를 처리할때는 저장된 포인터를 GetWindowLong 함수로 다시 가져와 사용하는 모습을 볼수 있다.

    이것은 다이얼로그 박스에서 사용할 수 있는 방법이다.

    위에서 사용한 아이디어로 다른 컨스텀 컨트롤에도 적용할 수 있다.

    2007/03/15 19:10 2007/03/15 19:10
    Posted
    Filed under About Knowledge/Programs_C++

    아래의 번역된 글을 이용하여 글을 올린다.

    번역문 : http://cafe.naver.com/ArticleRead.nhn?articleid=213&sc=e0d437180f462b9619&clubid=10426365

    역    자 : sobahoko
    ==================================================================================================



    원문 : Window Procedures as Class Member Functions 

    원문위치: http://www.rpi.edu/~pudeyo/articles/wndproc/index.html  

    원저자 : Oleg Pudeyev 

    원문 마지막 수정일: 2003 1 29 

    번역자 : sobahoko 

    번역시작일 : 2006.3.11 

    번역 마지막 수정일 : 2006.3.18  

    클래스 멤버함수를 윈도우 프로시저로 사용하기

     

    Oleg Pudeyev  

     

    문서에서, (Oleg Pudeyev) 클래스 멤버함수를 윈도우 프로시저로 사용하는 몇가지 방법에 대해 살펴볼 것이다. 나는 특정 메시지를 특정 멤버함수에 멥핑하는 방법에 대해서는 언급하지 않을 것이다. 모든 예제 코드에서, 하나의 윈도우 프로시저가 모든 메시지를 처리할것이며, 기본적인 처리를 위해서는 DefWindowProc 함수를 호출할 것이다. 여기서 살펴볼 방법들은 쉬운것에서부터 시작하여 복잡한 순으로 나열된다. 문서의 목차는 다음과 같다.

    기본 코드 

    정직한 방법 

    전역 변수 

    윈도우 고유 데이터 

    CBT Hooks 

    전역 헨들  

    MFC 접근법 

    ATL 접근법 

     

     

    기본 코드 

    나는 글을 진행하기 위해 간단한 Win32 프로그램을 출발점으로 사용하려고 한다. 프로그램은 윈도우를 열고 Hello, World!”를 WM_PAINT 메시지에 반응하여 출력하는 기능을 가지고 있다. 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/basecode.cpp )에서 있다

     

    정직한 방법 

    비록 제대로 동작하지 않지만, 윈도우 프로시저를 클래스 내부에 위치시키는 방법은 정직하게 다음처럼 하는 것이다

     

    class Window 

           LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); 

           // ... 

    }; 

     

    LRESULT CALLBACK Window::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) 

           // Access member variables here 

     

    // ... 

    WNDCLASS wc = { 

           // ... 

           Window::WndProc, 

           // ... 

    }; 

    // ... 

     

    코드의 전체 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/obvious.cpp )에서 있다. MSVC7에서 컴파일러는 컴파일에 실패하고 다음 에러 메시지를 낸다

     

    obvious.cpp(81) : error C2440: 'initializing' : cannot convert from 

                                   'LRESULT (__stdcall Window::* )(HWND,UINT,WPARAM,LPARAM)' to 

                                   'WNDPROC' 

         None of the functions with this name in scope match the target type 

     

     

    MSVC6 다음 에러 메시지를 낸다

     

    obvious.cpp(82) : error C2440: 'initializing' : cannot convert from 

                           'long (__stdcall Window::*)(struct HWND__ *,unsigned int,unsigned int,long)' to 

                           'long (__stdcall *)(struct HWND__ *,unsigned int,unsigned int,long)' 

         There is no context in which this conversion is possible 

     

    에러는 멤버함수가 전역함수와 다르기 때문에 발생한다. 멤버함수는 숨겨진 this 파라메터를 가지며, 멤버 함수가 호출되었을 그것을 통해서 클래스 인스턴스를 참조한다. 클래스 멤버가 아닌 함수들은 그런 파라메터가 없으므로, 멤버함수와 다르다

     

    정직한 방법을 간단히 수정 

    에러 메시지는 다음 코드와 같이 멤버함수 WndProc static 으로 만들면 사라진다

     

    class Window 

           // ... 

           static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); 

           // ... 

    }; 

     

    이것으로 Window::WndProc 함수로 WNDCLASS 구조체를 초기화 하는데 사용할 있다. 그러나, WndProc 함수가 static 이므로, 함수는 HelloString 변수와 같은 인스턴스 내부의 변수들에 접근할 없다. 그러므로 이것은 완전한 해결책이 아니다

     

     

    전역 변수 

    이것을 해결하는 간단한 방법은 메시지를 처리하는 클래스의 전역 인스턴스를 만들고, 메시지를 전역 인스턴스의 WndProc 함수로 전달해 주는 보조 함수를 만들어 사용하는 것이다. 코드는 아래와 같다

     

    Window w; 

    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) 

           return w.WndProc(hwnd, msg, wp, lp); 

     

    방법은 명확하며 신뢰할 만한 방법이다. 그리고 윈도우 클래스의 인스턴스가 오직 하나일 적합한 방법이다. 특별히 응용프로그램에 오직하나의 윈도우가 존재할 , 방법은 좋은 선택이 것이다. 그러나 방법은 클래스의 인스턴스가 여러개이며 메시지가 각각의 인스턴스에서 다르게 처리되어야 하는 상황에 사용할 없다. 이것에 대한 소스코드는 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/global.cpp ) 에서 있다

     

     

    윈도우 고유 데이터 

    Windows 의해서 생성된는 모든 윈도우의 인스턴스에는 메모리블럭을 할당할 있다. 메모리블럭과 블럭에서 참조되는 변수들을 통칭하여 윈도우 고유 데이터(Per-Window Data) 부른다. WNDCLASS 구조체의 cbWndExtra 멤버에 적절한 값을 주어, 데이터 영역의 크기를 지정할 있으며, 읽을때는 GetWindowLong 이나 GetWindowLongPtr 사용하고 쓸때는 SetWindowLong 이나 SetWindowLongPtr 사용한다. Ptr 붙은 버전은 64-bit Windows 호환되기 때문에 사용이 권장되며, 상위 호환성을 염두에 둔다면 Ptr 붙은 버전을 사용해야 한다

     

    우리는 윈도우 고유 데이터 영역에 객체의 포인터를 저장하여 특정 Window 객체와 윈도우를 연결시키려고 한다. , 어떤 시점에는 연결을 위해서 다음 함수를 호출하고 

     

    SetWindowLong(hwnd, GWL_USERDATA, this); 

     

    후에는 Window 포인터를 가져오기 위해서 다음함수를 호출한다는 얘기다

     

    Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA); 

     

    그러므로 전역 WndProc 함수는 다음과 같을 것이다

     

    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) 

           Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA); 

           if (w) 

                   return w->WndProc(hwnd, msg, wp, lp); 

           else 

                   return DefWindowProc(hwnd, msg, wp, lp); 

     

    if 문은 연결이 완료되기 전에 WndProc 함수가 호출되었을 경우를 처리하기위해 필요하다. 연결은 언제 완료되는가? 언제 SetWindowLong 호출하는가? 아마도 다음 코드와 같이 CreateWindow 호출한 직후에 SetWindowLong 호출하고 싶은 생각이 들것이다.  

     

    Window w; 

    HWND hwnd = CreateWindow(TEXT("BaseWnd"), TEXT("Hello, World!"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hinst, 0); 

    if (!hwnd) 

           return -1; 

    SetWindowLong(hwnd, GWL_USERDATA, &w); 

     

    불행히도, 방법은 WM_CREATE 메시지를 포함한 많은 메시지를 처리하지못한다. 메시지들은 CreateWindow 함수가 리턴되기 전에 발생하여, 결과로 WndProc 함수가 호출되기 때문이다

    그러므로 좀더 이른 시점에 연결을 완료하는 것이 필요하다

     

    WM_NCCREATE 희망을 준다. MSDN 에서는, 이것은 WM_CREATE 전에 보내지며, 우리가 this 포인터를 저장하는데 사용하려고 하는 CREATESTRUCT 포인터를 파라메터로 가진다고 말하고 있다. CreateWindow 함수는 lpParam 파라메터가 있다. 이것은 CREATESTRUCT lpCreateParam 멤버와 같은 파라메터이다. Window 객체 w 포인터를 lpParam 전달함으로써, 우리는 나중에 lpCreateParam 통해서 그것을 다시 가져올 있다. 포인터를 가지고 있으므로, 우리는 WM_NCCREATE 헨들러에서 SetWindowLong 함수를 호출할 있다. WM_CREATE 메시지가 왔을때는, 객체의 포인터를 GetWindowLong 함수를 통해 가져오고, 이것을 통해 멤버함수 호출이 가능하다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/windowdata.cpp )에서 있다

     

    접근법에는 한가지 단점이 있다. 그것은 WM_NCCREATE 메시지가 WndProc 함수가 받는 번째 메시지가 아니라는 사실이다. 만약 GetWindowLong에서 반환된 값이 NULL 아님을 확인하지 않고 멤버 변수를 사용하면, this 포인터가 NULL , 프로그램이 다운될 것이다. 정확이 얘기하면 윈도우 프로시저는 WM_NCCREATE 전에 WM_GETMINMAXINFO 메시지를 먼저 받는다

     

     

    윈도우 고유 데이터 해결책의 최적화 

    static WndProc 함수가 항상 if 문장을 평가해야 한다는 것은 쉽게 있다. 그러나 this 포인터가 설정되고 나면, 어떤 코드가 실행되어야 하는지 우리는 정확히 한다. 두개의 WndProc 함수를 사용하면 코드는 미세하게 빨라진다. 예제 코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/windowdata2.cpp )에서 있다

     

     

    전역 임시 this 포인터 저장소 

    방법은 전역 WndProc 함수가 받는 모든 메시지에 대해서 멤버 WndProc 함수가 호출되도록 하는 방법이다. 아이디어는 Magmai Kai Holmlor 것이다. ( GameDev.net thread 참조 : http://www.gamedev.net/community/forums/topic.asp?topic_id=59171

    방법에서는 this 포인터를 임시 전역 변수에 저장한 전역 WndProc 함수가 처음으로 실행되었을 SetWindowLong 호출하는 방법이다. 방법이 위에서 언급한 전역변수 방법보다 나은 이유는 전역변수를 짧은 시간 동안만 사용하므로 많은 윈도우를 생성할수 있으며, 모든 윈도우들이 같은 전역변수를 사용하기 때문이다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd.cpp ) 참조할것

     

    방법은 불행히도 쓰레드안정성을 가지고 있지 않다. 만약 두개의 쓰레드가 동시에 윈도우를 생성할 데이터가 훼손될 있으며 프로그램이 다운될 있다. 이것을 피하는 방법은 전역변수를 보호할 critical section 사용하는 것이다. 이것에 대한 소스코드는 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2.cpp ) 참조할

    여전히 코드는 만족스럽지 않다. 다음 시나리오에서 두개의 쓰레드는 데드락상태가 있다

     

    쓰레드 A 윈도우를 생성한다

    윈도우의 WM_CREATE 헨들러에서, 쓰레드 A 쓰레드 B 생성하고 쓰레드 B 의해서 이벤트가 설정되기를 기다린다

    쓰레드 B 다른 윈도우를 생성하고, 이벤트를 설정한다. 그리고 뭔가 다른 일을 한다

     

    쓰레드 B 윈도우를 생성하려고 , 이미 쓰레드 A 잡고 있는 critical section 진입하려고 한다. 이것은 데드락을 발생시킨다. 문제는 두가지 방법으로 해결할 있다. 두가지 방법 모두 궁극적으로 쓰레드가 전역 critical section 락을 잡고 있는 시간을 줄이는 방법을 사용한다

     

    1. 메시지 헨들러에서 번째 메시지가 들어오면 critical section 떠난다. 방법은 기존 코드를 그리 많이 수정하지 않아도 된다. 그러나 윈도우 객체의 멤버에 플래그를 추가하는 것이다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2m.cpp ) 참조할 . 플래그는 윈도우 생성이 성공했든, 실패했든지간에 critical section 오직 한번만 해제되었다는 것을 보증하기 위한 것이다. 방법은 복잡하며 유지관리가 편하지 않다.  

    2. 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2c.cpp ) 구현된 방법은 보조 컨테이너를 사용하여 쓰레드 아이디와 윈도우 객체 포인터가 저장된 멥에 대한 접근을 동기화시킨다. 방법은 동기화 코드를 정의된 범위에 집어 넣어 critical section lock 걸리는 시간을 최소화한다. , 멥을 조회하고 멥에 삽입하는 짧은 시간 동안만 락을 건다. 방법에서 하나의 명확한 부작용은 thread-local storage 전혀 사용하지 않는다는 것이다

     

    모든 활성 쓰레드에 대해 전역 포인터를 중복가능하게 하는 대안으로 다음코드와 같이 __declspec(thread) 수정자를 사용할 있다.  

     

    __declspec(thread) Window *g_pWindow; 

     

    불행히도 dll 저장된 데이터에 대해서는 __declspec(thread) 수정자를 사용할 없다. 방법은 오직 실행파일(exe) 존재하는 코드에 대해서만 적용가능하다

     

     

    CBT Hooks 

    CBT hooks Window 객체의 포인터를 윈도우 고유 데이터에 저장할 있는 또다른 방법을 제시한다. CBT hook 윈도우 프로시저에 보내지는 모든 메시지보다 앞서 호출된다. 그러므로 hook에서 윈도우 객체 포인터를 윈도우 헨들에 연결시키는데 관한 모든 작업을 수행하는 것이 가능하다. 윈도우 프로시저는 포인터를 받기만 하면 된다

     

    윈도우가 생성되기 , 다음과 같이 hook 설정한다

     

    HHOOK hHook = SetWindowsHookEx(WH_CBT, CBTProc, 0, GetCurrentThreadId()); 

     

    작업은 윈도우가 생성될때마다 수행되는 것이 아니라, 쓰레드 실행중 한번만 수행되어야 한다. 그러나 코드를 단순하게 하기위해, 윈도우가 생성될때마다 수행하는 방식으로 예제를 작성하였다. CreateWindow 함수가 호출되면, CBTProc 호출된다. CBTProc 내부에서, this 포인터를 받아 전역변수, thread-local storage 또는 다른 기법을 사용한후, SetWindowLog 호출한다. 이방법을 보여주는 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/hook.cpp ) 참조할것

     

     

    전역 헨들  

    다른 방법은 모든 윈도우에 대해서 헨들-포인터 쌍을 저장하는 하나의 전역 멥을 사용하는 것이다. HWND 그에 대응하는 Window 포인터로 구성된 것이 Window 생성될때마다 멥에 추가되고, Window 객체가 소멸될 맵에서 삭제되는 방식이다. 그러면 클래스는 HWND 해당하는 Window 포인터를 돌려주는 방법을 제공할 수있다. 그런 예를 소스코드( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2c.cpp ) CPtrMap에서 있다. 구현은 매우 직관적이지만 모든 메시지가 처리될때마다 포인터를 조회하고, 필요한 부가작업을 수행하는 것은 잠재적으로 효율이 떨어진다. 응용프로그램에서 많은 수의 윈도우를 열고 있을 vector list 사용하면 선형시간( O(N) ) 소요되고, map 사용하면 로그시간( O(log N) ) 소요된다. hash map 상수시간 ( O(1) ) 조회를 수행할 있지만, 다른 단점를 가지고 있다. MFC에서 하는것처럼 쓰레드 마다 분리된 멥을 사용하면 조회시간을 최적화하는 것이 가능하다. 사용자 레벨에서 헨들을 포인터로 변환( MFC FromHandle FromHandlePermanent 함수 )하는 용도의 전역 헨들 방법은 윈도우 관리 코드에 의해 내부적으로 헨들을 포인터에 맵핑하는 다른 해결책과 동시에 사용하는 것이 가능하다. 또한 사용자에게 그런 변환기능을 제공하는 것이 반드시 필요한 것은 아니다. 사실 ATL 에는 그런 변환기능이 없지만 아무 문제가 없다. 마지막으로 방법 하나만으로는 실제로 동작하지 않으며 다른 방법들도 마찬가지다. 누군가 헨들과 포인터를 연결시켜야하고, 거기에는 헨들과 거기에 대응하는 포인터가 필요하다. 그러므로, 임시 윈도우 프로시저, hook 또는 다른 방법이 앞서 언급된 연결을 확립하는데 반드시 필요하다. 간단한 예를 들어, CreateWindow 함수를 호출한 다음 맵을 생성할 수도 있다. CreateWindow 실행되는 동안 발생되는 메시지를 처리할 필요가 없다면, 방법도 사용할 있는 해결책이 될것이다

     

    MFC 접근법 

     

    MFC 윈도우 헨들을 객체에 연결시키는 방법을 알아보기위해, 나는 다음 MFC 코드를 사용했다

    (http://www.rpi.edu/~pudeyo/articles/wndproc/mfc.cpp

    UI 그리 멋진 것은 아니지만, 프로그램은 나의 목표를 달성하기에 충분하다. MFC dll 내부를 탐험하기위해 디버거에서 실행시킨다.  

     

    번째 눈에 들어오는 것은 wincore.cpp 파일에 있는 AfxHookWindowCreate 함수이다. 함수는 윈도우가 CWnd::CreateEx 함수에의해서 생성될 함수내부에서 호출되어, 윈도우 객체를 MFC hook 한다. MFC CBT hook 사용하여 현재 쓰레드의 윈도우 생성을 hook 하는 것을 수있다. CBT hook 위에서 언급했기 때문에 낯설지 않을 것이다. MFC hook handle 저장하기 위해 thread state block 사용한다. thread state block thread 실행되는동안 hook 관리하기 때문이다. MFC 어떻게 데이터를 전달하는지에 주목하기 바란다. 모든 thread per-thread state structure 가지고 있다. ( _AFX_THREAD_STATE, afxstat.h ) 여기에 모든 thread-specific MFC data 저장하는데, 현재 생성중인 윈도우에 대한 정보도 저장된다. thread state structure 포인터를 저장하는 동적 배열에 저장된다. 배열에 대한 포인터는 TlsXXX 계열의 함수를 통해 저장되고 참조된다. AFX thread-local storage 대한 구현상세는 afxtls_.h, afxtls.cpp에서 볼수 있다. hook data exchage 얘기로 다시돌아와서, CreateEx 함수는 CWnd 객체의 포인터를 thread state 저장하고, _AfxCbtFilterHook 함수는 그것을 thread state 로부터 가져온다.  

    hook 이제 thread state block으로부터 가져온 CWnd 포인터와 그것에 대응하는 HWND 가지게 된다. CWnd::Attach 함수를 호출하여 CWnd HWND 연결하고, HWND CWnd 쌍을 per-thread handle map 저장한다. ( per-thread 라는 단어에 주목하기 바란다. 이것이 multi-thread 환경에서 CWnd 그밖의 window resource wrapper 객체를 공유하지 못하는 이유이다. 특정 thread CWnd 다른 thread HWND-CWnd map 존재하지 않는다. 만약 CWnd 객체를 생성한 thread 아닌 다른 thread에서 CWnd 포인터를 통해 HWND lookup 한다면, 코드는 실패할 것이다. 그러나 HWND 다른 헨들은 thread 간에 전달 할수있으며, CWnd::Attach 함수를 호출하여 CWnd HWND 연결시킬 있다. 그러나 thread 다르므로, CWnd 객체는 다른 것이 될것이다. ) 그런다음, MFC 윈도우를 subclass 하고 thread 고유한 현재 생성중인 윈도우를 가리키는 포인터를 null 설정한다. CWnd 이제 메시지를 처리할 있는 상태가 된다

     

    윈도우가 메시지를 받으면, 메시지는 AfxWndProcBase 함수를 거쳐, AfxWndProc 함수로 보내진다. AfxWndProc 함수는 window handle map 으로부터, 전달받은 HWND 조회하여 CWnd 얻기위해 CWnd::FromHandlePermanent 함수를 사용한다. map thread-specific 하다. 그러므로 윈도우를 생성한 thread 만이 윈도우에 보내진 메시지를 처리할 있다. 그후 HWND 헨들을 받는 AfxWndProc 함수는 AfxCallWndProc 함수를 호출하는데, 이함수는 HWND 헨들대신 CWnd 포인터를 받는다는 점만 다르다. 그후 AfxCallWndProc 함수는 CWnd::WindowProc 함수에게 메시지를 보낸다. 그것을 받은 CWnd::WindowProc 함수는 CWnd::OnWndMsg 함수를 호출한다. CWnd::OnWndMsg 함수는 지정된 윈도우의 메시지맵에서 헨들러를 찾고, 메시지 파라메터를 decode 하고, 헨들러를 호출하여, 사용자가 작성한 OnPaint 함수에 이르게 된다

     

     

    ATL 접근법 

     

    나는 ATL 메시지 처리 구조를 조사하기위해 다음코드를 사용했다. ( http://www.rpi.edu/~pudeyo/articles/wndproc/atl.cpp ) 디버거에서 실행하고 CWindowImpl::Create ( atlwin.h ) 함수안으로 들어간다. 잠시후 AtlWinModuleAddCreateWndData (atlbase.h) 함수에 이르게 되는데, 함수는 _AtlCreateWndData 구조체 포인터를 리스트에 저장한다. 구조체 리스트에는 윈도우 객체 포인터들이 저장되는데, WM_NCCREATE 메시지가 도달하기 전에 ATL window procedure 들이 접근하여 사용할 있다. 모든 구조체는 thread identifier 거기에 속한 윈도우 객체 포인터를 가지고 있다. MFC 에서는 실제로 윈도우를 생성중이건 아니건간에, 모든 thread 현재 생성중인 윈도우에 대한 참조를 가지고 있지만, ATL 전체 application 대하여 하나의 리스트가 그것을 관리한다. 이런 방식은 하나의 UI thread 많은 worker thread 존재할 약간의 메모리를 절약 있다

     

    이제 static 멤버함수 CWindowImplBaseT::StartWndProc (atlwin.h) 이른다. 이것은 현재 윈도우에 보내진 번째 메시지를 처리하는 메시지 헨들러 이다. 여기서 CWindow HWND 쌍을 연결하는 작업을 하여, 현재 메시지와 앞으로 메시지들이 멤버 윈도우 프로시저로 route 되도록 한다. 지체 없이 create-window-data structure 리스트로부터 AtlWinModuleExtractCreateWndData (atlbase.h) 함수를 사용하여 this 포인터를 가져온다. 함수는 current thread identifier 리스트를 조회한다. thread identifier 일치하면 거기에 대응하는 window object pointer 리턴된다. 다른 ATL 코드나 사용자 코드가 실행되기 전에 가져오기 때문에 특정 thread 에는 오직 하나의 window object pointer 연결된다. 이로써 CreateWindow 함수는 번째 받은 메시지로부터 호출되게 된다. ATL 여러개의 thread 동시에 리스트에 접근하는 것을 방지하기 위해 critical section 사용한다. window 생성 과정전체 동안이 아니라, create window data 추가하고 가져오는 동안에만 lock 하기 때문에 다른 thread 들이 필요없이 기다리지 않는다

     

    이제 CWindowImplBaseT::StartWindowProc 함수는 this 포인터를 가지게 되었다. CWindowImplBaseT::GetWindowProc 함수를 사용하여 실제 윈도우 프로시저를 가져온다. 그다음 thunk 생성하는데, 이것의 목적은 HWND 헨들을 CWindow 포인터로 변환하는 것이다. 그것은 다음과 같이 이루어진다

     

    StartWindowProc 함수는 window object instance data 일부인 특정 메모리를 특정 코드로 초기화한다. 코드는 임시 윈도우 프로시저처럼 동작하는 코드로, 들어오는 HWND CWindow 포인터로 멥핑한다. 그런 다음 윈도우 프로시저는 ( SetWindowLong 함수를 통해서 ) 메모리 블록의 시작지점을 가리키도록 설정된다.  

     

    특정 메모리는 다음과 같이 평범한 프로토타입의 윈도우 프로시저를 저장하는 어떤 것이다

    LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); 

     

    윈도우 메시지가 프로시저에 도달했을 , 코드는 hwnd parameter CWindow object 포인터로 변환한다. CWindow object 주소는 같은 메모리블럭에 저장된다

    그다음 메시지 프로시저는 실제 static member procedure jump 한다. 그것은 WindowProc 함수와 동일한 프로토타입을가지고 있지만 hwnd parameter 실제 윈도우 헨들이 아니라, CWindow pointer 이다. 그함수는 포인터를 얻기위해 cast 수행한다

    CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; 

     

    다음은 atlbase.h 있는 _stdcallthunk thunk code 이다

     

    mov dword ptr[esp+4], pThis 

    jmp WndProc 

     

    pThis Init 파라메터로 전달된다. 이것은 현재윈도우의 CWindow 객체에 대한 포인터이다. WndProc 또한 파라메터로 전달된다. 이것은 실제 WndProc 주소이며, 대부분의 경우 이것은 CWindowImplBaseT::WindowProc (atlwin.h) 된다. Init 코드를 실행시에 주어진 두가지 정보를 가지고 build 한다. 코드는 다음과 같은 이유로 CWindow 객체의 일부인 메모리블럭에 저장된다

     

    1. Code segment 읽기전용이며, 메모리에 있는 코드를 수정하는 것은 바이러스처럼 보인다

    2. 윈도우가 다르면 윈도우 프로시저도 다르다. 그리고 윈도우 프로시저가 가진 CWindow 포인터는 모두 다르다. 그러므로 하나의 코드가 종류가 다른 윈도우를 처리하는데 사용될 수는 없고, 코드는 실행시에 생성되거나 복제되어야 한다

     

     

    코드가 하는 일은 hwnd parameter thunk 속한 객체의 포인터를 사용하여 window procedure 교체하는 일이다. (CWindowImplRoot, atlwin.h) 그리고 static 멤버함수 WndProc 으로 jump 하는데, 함수의 번째 파레메터는 HWND 가장한 CWindow 포인터이다. 물론 컴파일러는 에러를 발생시키지 않는다. ATL 구현은 멤버 윈도우 프로시저를 가져오는 부분에 있어서는 MFC 비해 매우 효율적이다. MFC 코드는 윈도우 객체를 조회하는데 윈도우의 개수에 비례하는 선형시간이 소요되는 반면, ATL 상수시간이 소요된다. ATL 코드는 하나의 move, 하나의 jump 사용하여, 최대한 효율적이다

     

     

    MFC ATL 비교 

     

    위에서 언급했듯이, ATL MFC 구현에는 다음과 같은 차이점이 있다

     

    ATL thread-local storage 사용하지 않는다. 그러므로 어떠한 초기화도 사용하지 않는다. MFC 사용하면 AfxWinInit 으로 초기화를 반드시 해야한다. ATL 사용자 코드를 직접적으로 수행한다. 게다가 TLS 접근하는 것은 동적 배열을 사용하므로, 상각된 상수시간( amotized constant time ) 소요되는데, 이것은 MFC 실행 속도를 감소시킬 있다

     

    ATL 맵핑은 thread-specific 하지 않다. 프로그래머는 CWindow 객체를 만든 thread에서 뿐만 아니라, 모든 thread에서 사용할 있다. MFC CWnd 같은 객체를 그것을 생성한 thread bind 한다. MFC 포인터를 CWnd* 형으로 전달하는반면, ATL 함수 파라메터로 HWND 헨들을 전달한다

    MFC WndProc 간접호출이 많아 오버헤드를 좀더 가진다. ATL 조금더 빠르다

    MFC 주어진 HWND CWnd 포인터를 조회하는데 상수시간이 소요된다. 응용프로그램에 윈도우가 다수존재할 경우 이것은 그리 빠른성능을 보이지 않는다. MFC WndProc 윈도우의 개수가 많아 질수록 속도가 느려지는 반면, ATL 윈도우의 개수에 상관없이 동일한 고성능을 보인다

    2007/03/15 19:07 2007/03/15 19:07
    Posted
    Filed under About Knowledge/Programs_C++

    // 따로 예제를 작성했습니다.

    // 참고하시기 바랍니다.

    #include "windows.h"


    LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

    HINSTANCE g_hInst;

    LPSTR lpszClass="Button Subclass 1";

    HWND g_hWndMain;


    int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

                        ,LPSTR lpszCmdParam,int nCmdShow)

    {

       HWND hWnd;

       MSG Message;

       WNDCLASS WndClass;

       g_hInst=hInstance;

       

       WndClass.cbClsExtra=0;

       WndClass.cbWndExtra=0;

       WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);

       WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

       WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

       WndClass.hInstance=hInstance;

       WndClass.lpfnWndProc=(WNDPROC)WndProc;

       WndClass.lpszClassName=lpszClass;

       WndClass.lpszMenuName=NULL;

       WndClass.style=CS_HREDRAW | CS_VREDRAW;

       RegisterClass(&WndClass);

       

       hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,

           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

           NULL,(HMENU)NULL,hInstance,NULL);

       g_hWndMain=hWnd;

       ShowWindow(hWnd,nCmdShow);

       

       while(GetMessage(&Message,0,0,0)) {

           TranslateMessage(&Message);

           DispatchMessage(&Message);

       }

       return Message.wParam;

    }


    #define  ID_BUTTONS_BEGIN    300


    typedef struct

    {

    int  nNum;

    int  nClick;

    }Button_Data;


    Button_Data  *pBtnData=NULL;

    int      nButton_num=100;

    int      iButtonWidth=25;

    int      iButtonHeight=25;

    HWND    *pButtons=NULL;

    WNDPROC  OldEditProc;

    HWND     hButton_Main=NULL;

    char   *pButtonPropName="Button_Prop_Name";


    LRESULT CALLBACK ButtonSubProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

    {

       char         str[256];

       Button_Data  *pBDataTmp=NULL;

       switch (iMessage)

       {

       case WM_RBUTTONDOWN:

           pBDataTmp=(Button_Data *)GetProp(hWnd,pButtonPropName);

           wsprintf(str,"%lu",(DWORD)pBDataTmp->nNum);

           

           switch(pBDataTmp->nClick)

           {

           case 0:

               SetWindowText(hWnd,"★");

               pBDataTmp->nClick=1;

               break;

           case 1:

               SetWindowText(hWnd,"♣");

               pBDataTmp->nClick=2;

               break;

           default:

               SetWindowText(hWnd,"♠");

               pBDataTmp->nClick=0;

           }


           // 아래에서 return문을 쓰면 메시지가 버튼에 전달되지 않습니다.

           // 메시지를 막을 이유가 없으면 break문을 써서 CallWindowProc함수를 통해

           // 버튼윈도우에 메시지가 전달되도록 해줘야 합니다.

           break;

       }

       return CallWindowProc(OldEditProc,hWnd,iMessage,wParam,lParam);

    }


    LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

    {

       PAINTSTRUCT  ps;

       HDC          hdc;

       int          i;

       int          x, y;

       char         Mes[]="버튼의 마우스 오른쪽 버튼 클릭을 검출합니다";

       

       switch(iMessage)

       {

       case WM_CREATE:

           pButtons=(HWND *)malloc(sizeof(HWND)*nButton_num);

           

           if (pButtons==NULL)

           {

               MessageBox(hWnd,"Error!","Error!",MB_OK);

               DestroyWindow(hWnd);

           }


           pBtnData=(Button_Data *)malloc(sizeof(Button_Data)*nButton_num);

           if (pBtnData==NULL)

           {

               free(pButtons);

               MessageBox(hWnd,"Error!","Error!",MB_OK);

               DestroyWindow(hWnd);

           }

           

           // 전역 서브클래싱을 위한 버튼을 하나 만듭니다.

           // 이 버튼은 하는 일이 없이 단지 서브클래싱을 하기위해 생성한 것이므로

           // 크기를 0으로 만든다음 윈도우의 바깥으로 보내서 감춥니다.

           hButton_Main=CreateWindow("button",NULL,WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,

                                    -1,-1,0,0,hWnd,(HMENU)0,g_hInst,NULL);

           

           // 버튼 윈도우를 전역서브클래싱을 합니다.

           OldEditProc=(WNDPROC)SetClassLong(hButton_Main,GCL_WNDPROC,(LONG)ButtonSubProc);

           

           // 서브클래싱된 버튼윈도우들을 만듭니다.

           for(i=0; i < nButton_num; i++)

           {

               x=(i%10)*iButtonWidth;

               y=(i/10)*iButtonHeight;

               

               pBtnData[i].nClick=0;

               pBtnData[i].nNum=i;


               pButtons[i]=CreateWindow("button","♠",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,

                                       x+10,y+40,iButtonWidth,iButtonHeight,hWnd,

                                       (HMENU)ID_BUTTONS_BEGIN+i,g_hInst,NULL);


               // 버튼의 윈도우프로퍼티에 버튼에 대한 정보를 담은 구조체의

               // 주소값을 저장합니다.

               SetProp(pButtons[i],pButtonPropName,(HANDLE)(pBtnData+i));

           }

           

           SetFocus(pButtons[0]);

           return 0;

           

       case WM_PAINT:

           hdc=BeginPaint(hWnd, &ps);

           TextOut(hdc,10,10,Mes,strlen(Mes));

           EndPaint(hWnd, &ps);

           return 0;

           

       case WM_DESTROY:

           if (pButtons)

           {

               for(i=0; i < nButton_num; i++)RemoveProp(pButtons[i],pButtonPropName);

               free(pButtons);

           }

           if (pBtnData) free(pBtnData);

           SetClassLong(hButton_Main,GCL_WNDPROC,(LONG)OldEditProc);

           PostQuitMessage(0);

           return 0;

       }

       return(DefWindowProc(hWnd,iMessage,wParam,lParam));

    }

    2007/03/06 20:38 2007/03/06 20:38
    Posted
    Filed under About Knowledge/Programs_C++

    1. CString -> double

    #include "stdlib.h" // atof() 사용을 위해

    // CString형을 double형으로 변환
    double CString2double(CString str)
    {
           char *Temp = (LPSTR)(LPCSTR)str;
           return atof(Temp);
    }

    2. double -> CString

    // double형을 CString형으로 변환
    CString double2CString(double num)
    {
           CString Temp;
           Temp.Format("%f", num);
           return Temp;
    }

    3. CString -> char*

    // CString형을 char* 형으로 바꾸는 함수
    char *CString2char(CString str)
    {
           char *charTemp = (LPSTR)(LPCSTR)str;
           return charTemp;
    }

    4. int -> char *

    Visual C++ 에는 다음과 같은 함수가 있습니다. int , __int64 , unsigned __int64 등의 데이터형을 char* 형으로 바꾸어주는 함수들입니다.

    인자에 대해 설명하자면 int value 는 입력값, char *string 은 출력값, int radix 는 출력될 진법을 표시합니다. 예를 들어 radix를 16으로 설정하면 16진수로 출력한다는 말이죠. radix 값은 2~36 사이의 값만 가능합니다.

    #include "stdlib.h"

    char *_itoa( int value, char *string, int radix );

    char *_i64toa( __int64 value, char *string, int radix );

    char * _ui64toa( unsigned __int64 value, char *string, int radix );

    wchar_t * _itow( int value, wchar_t *string, int radix );

    wchar_t * _i64tow( __int64 value, wchar_t *string, int radix );

    wchar_t * _ui64tow( unsigned __int64 value, wchar_t *string, int radix );


    5. char* -> double

    #include "stdlib.h"

    // char* 형을 double 형으로 바꾸는 함수
    double atof( const char *string );


    6. char* -> int

    #include "stdlib.h"

    // char* 형을 int 형으로 바꾸는 함수
    int atoi( const char *string );


    7. char* -> __int64

    #include "stdlib.h"

    // char* 형을 __int64 형으로 바꾸는 함수
    __int64 _atoi64( const char *string );


    8. char* -> long

    #include "stdlib.h"

    // char* 형을 long 형으로 바꾸는 함수
    long atol( const char *string );

    2007/02/12 19:18 2007/02/12 19:18