본문 바로가기
Windows/MFC 강좌 & Tips

Dialog based 그림판

by 미티치 2017. 1. 4.

 

1. 글을 쓸 수있는 컨트롤이 Edit Text라면, 그림을 그릴 수 있는 컨트롤은 Picture Control

- 배경을 하얀색으로 하기 위해서는 속성 >> Type : Rectangle , Color : White로 지정

 

 

2. 직선그리기 기능 추가

 

  • dc.MoveTo( ) 는 기준 점의 위치를 특정 좌표로 이동시키는 함수
  • dc.LineTo ( ) 는 함수에 의해 설정된 기준 점 좌표로부터 직선을 그려주는 함수
    => dc를 얻어왔을 때 최초로 그려지기 시작하는 위치는 (0,0) 이다. 이 때, MoveTo( ) 함수는 그려지기 시작할 위치를 설정해주고, 이 위치부터 LineTo( ) 함수로 지정한 곳까지 그려진다. 따라서 선을 그리려면 MoveTo( ) 함수를 사용해서 어디서부터 그려지기 시작할 지를 설정한 후, LineTo( ) 함수를 사용해서 끝점을 지정하면 된다.

     

  • Picture Control에 그림을 그려줘야 하므로, CMyPaint.cpp 에 PictureControl의 포인터를 선언해서 메소드를 사용하려는 삽질을 하루동안 했는데, 사실 중요한 것은 그림을 그릴 수 있는 Dialog로 보이는 것. 이말은 즉, 그림을 그리기 위해서 Picture Control을 사용할 필요가 없다는 것을 의미한다.  하루동안 Picture Control 의 포인터를 받아와서 선을 그렸는데, 다이얼로그 로드 시 거치는 함수 OnPaint( ) 에 선을 그리는 작업을 하면 잘 그려지긴 했는데 마우스를 이용해서 선을 그리기 위해 LButtonDown 이벤트 처리에 다음과 같은 소스를 넣으면 먹히질 않았다.
  첫번째 문제는 Picture Control에 LButtonDown 처리가 없는데도 불구하고 여기에 마우스로 그리는 작업을 하려고 LButtonDown 함수에서 열심히 Picture Control의 dc를 포인터로 받아와서 그림 그리려고 한점.  다시 말하지만, 그림을 그려야한다고 해서 Picture Control을 쓸 필요가 없다!!! 그냥 다이얼로그에 그리면 됨!ㅠㅠ 1번에서 추가한 컨트롤 필요없음!!!

  두번째 문제는 다이얼로그의 OnLButtonDown 메소드를 직접 손으로 구현한 점. 다이얼로그 리소스에서 속성 >> 메시지 >> WN_LButtonDown 에서 이벤트를 추가해줬어야 했는데, 메소드를 직접 적고나서 정작 MESSAGE_MAP에는 ON_WN_LBUTTONDOWN( ) 을 추가해주지 않아서 LBUTTONDOWN자체를 인식하지 못함. OnLButtonDown 메소드를 구현한다고 메소드가 실행되는 것이 아니고, 이벤트 발생 시 이 메소드를 찾아갈 수 있도록 MESSAGE_MAP에 ON_WN_LBUTTONDOWN( )메세지를 추가해줘야한다.

 

  • SetCapture( ) 함수는 마우스 커서 좌표가 활성화된 윈도우 영역을 벗어나더라도 지속적으로 마우스 메시지를 받아야 하는 경우 사용하는 함수이다. 내가 SetCapture 함수를 호출하기 전에 다른 윈도우가 SetCapture 함수를 사용하여 마우스 메시지를 점유하고 있었다면 해당 윈도우 객체의 주소를 반환하고, 그런 윈도우가 없었다면 NULL을 반환한다. 
    이 반환 값은 임시 객체일 가능성이 있기 때문에 전역 변수나 멤버 변수에 저장하여 계속 사용할 경우 문제가 발생할 수 있다.
  • 마우스 메시지를 점유하다가 해제하고싶을 때 사용하는 것이 ReleaseCapture( ) 함수
    => 일반적으로 SetCapture( ) 함수를 사용했다면 사용이 끝난 후에 ReleaseCapture( ) 함수를 호출해서 다른 윈도우와의 메시지 점유에 대한 충돌을 피하는 것이 좋다.   
                                                ( 출처 http://www.tipssoft.com/bulletin/board.php?bo_table=FAQ&wr_id=101 )

  • Invalidate( BOOL bErase = TRUE ) 함수 는 호출한 윈도우에 WM_PAINT 메시지를 발생시킨다. 즉, 원하는 윈도우의 화면을 강제로 갱신할 때 사용하는 함수이다.
    - 윈도우 화면을 갱신할 때는 두 가지의 메시지를 이용할 수 있다. WM_PAINT와 WM_ERASEBKGND 메시지이다. Invalidate 함수를 이용해서 WM_PAINT 메시지만 발생하고 싶으면 Invalidate(FALSE) ; 로 호출
    - WM_PAINT 와 WM_ERASEBKGND 메시지 둘 다 발생시키고 싶으면 Invalidate( ) ; 또는 Invalidate( TRUE ) ; 라고 호출하면 된다.
                                                ( 출처 http://www.tipssoft.com/bulletin/board.php?bo_table=FAQ&wr_id=48 )

 

 

 

 

 

3. 점 찍기 = 선 그리기 기능 추가 (Pen 기능)

  • CClientDC와 CPaintDC 의 차이점? 그리고 CDC는?
    참고 자료 http://blog.naver.com/PostView.nhn?blogId=skyarro&logNo=120095702323
  •  SetPixel ( ) 함수를 사용했더니 마우스 우클릭한 상태로 움직이면, 마우스의 움직임을 다 못따라 와서 깔끔하게 선형태로 선을 그려주지 못했다. 그래서 LineTo() 와 MoveTo()를 이용해서 아주 작은 직선을 연결해서 자연스러운 선을 그릴 수 있도록 하였다.
  • OnLButtonDown -> OnMouseMove -> OnLButtonUp 함수에 순서대로 구현
  • 또한 선을 그리고 Invalidate( ) 처리를 하면 선을 그릴 때마다 새로 그려지고, Invalidate( FALSE )로 처리를 하면 다른 작업을 하거나 다시 PEN 버튼을 눌러 선을 그리면 새로 다시 그려진다. 이게 무슨 말이냐면, 펜 버튼을 누르고 선을 그린다. 그리고 도형 버튼을 눌러서 다이얼로그에 도형을 여러개 그려놓고, 다시 펜 버튼을 눌러 선을 그리면 Invalidate 함수 때문에 화면이 다시 그려지기 때문에 도형 위에 선이 다시 그려지게 되더라. 따라서 이 기능에선 Invalidate ( ) 함수를 사용할 필요는 없었다.
    -> 하지만 직선을 그릴 때 Invalidate ( ) 함수를 사용하기 때문에 OnPaint 함수에서 사용자가 그린 선을 다시 그려주는 for문을 작성했다. 이부분이 없어도 선이 그려는 지지만 화면이 갱신되었을 때 다시 선이 보이기 위해서는 필요
  • 따라서, 중요한 것은 사용자가 그린 그림을 저장하는 것. 화면이 갱신되는 경우가 이전까지는 첫 화면 구성시와 Invalidate( ) 함수 호출 시라고 생각했는데, 그 외에도 화면을 다시 그려주는 경우를 위해 사용자가 그린 그림을 저장해놔야한다.

 

 

1 )  이전까지는 선을 그리기 위해서 Invalidate( ) 함수를 이용해서 메세지 큐에 있는 ON_WM_PAINT( ) 호출하였고, OnPaint( ) 안에서 dc를 이요해서 선을 그렸다. ( MoveTo와 LineTo를 이용해서 ) 이 과정에서 오해했던 것이 있다면, 그림을 그려주는 것은 dc를 이용해서 그리면 되고, 굳이 Invalidate 함수를 이용해서 화면을 순간순간 갱신해 줄 필요가 없다는 것이다. 

 

하지만 여기서 기억해야 할 것이 있다!! 이전에서 처럼 단순히 선을 그리고 이것을 화면에 보여주는 기능만을 할 때에는 상관이 없지만, 이 화면을 다른 윈도우와 겹쳤다가 다시 보여줄 때 그린 그림들이 지워진 현상을 겪게 될 것이다. 이것은 화면이 무효화 영역에서 벗어날 때 다시 그려주기 때문인데, 무효화 영역은 다음과 같다.

 

 

 

무효화 영역에 대한 복구 작업 소스가 추가되지 않은 상태에서, 그림판에 그림을 그린다.

 

 

 

   

 

이렇게 다른 윈도우 화면과 겹치거나, 혹은 작업표시줄에 화면이 가려지게 내리면 무효화 영역이 발생한다.

 

 

 

 

무효화 영역에 대한 복구 작업 소스가 없으면, 위와 같이 작업표시줄에 겹쳤던 부분의 그림이 지워진 것을 확인할 수 있다.

 

 

 

 

2 ) 생성된 윈도우가 다른 윈도우에 의해 가려졌다가 다시 나타날 경우, 가려졌던 부분이 지우개로 지운 것처럼 지워지는 현상이 나타나는데 이렇게 생성된 윈도우의 가려졌던 부분을 무효화 영역(Invalid Region) 이라 한다. 이렇게 무효화 영역이 생기면 사용자는 무효화 영역에 대한 복구를 해야한다.

 

 

3 ) 무효화 영역에 대한 복구 작업은 WM_PAINT 메시지를 이용해서 수행해야 한다. 무효화 영역이 생겼을 경우 windows는 WM_PAINT 메시지를 발생시켜서 무효화 영역을 처리하도록 하는 것이다.

 

여기서 참고해야 할 점은 WM_PAINT 메시지는 보통 메시지가 아니라 Flag 메시지이다. 즉, WM_PAINT 메시지는 처리를 위해 다른 무언가에 의해 발생 되어지는 메시지가 아니라 처리되어야 할 필요성, Flag만 설정되고 메시지 큐 안에 다른 메시지들 처리가 끝나면 처리되는 메시지라는 것이다. Flag성 메시지는 메시지가 발생하게 되면 처리되기 위해 메시지 큐에 들어가는 것이 아닌 처리되어야함을 의미하는 Flag만 설정하는 메시지를 의미한다. GetMessage 함수가 메시지 큐에 있는 멤시지들을 다 처리하고 나면 Flag성 메시지들을 검사하게 되는데 Flag성 메시지의 Flag가 설정되어 있다면 바로 처리하게 된다. 따라서 보통 메시지와는 다른 메시지 처리 방법을 갖고 있고, 보통 메시지들보다 처리되는 우선 순위가 가장 낮다.

 

 

4 ) WM_PAINT 메시지 발생 시점
    - 윈도우가 처음 생성될 때
    - 윈도우가 다른 윈도우에 가려졌다가 환원될 때
    - 윈도우 크기 조정 시
    - 윈도우의 위치가 이동되어 화면 밖으로 나갔다가 다시 나타날 때
    - 윈도우가 스크롤 될 때

 

 

5 ) 무효화 영역이 생기면, 윈도우 전체를 다시 그려주는 것이 아니라 (메모리 소모가 심해지니까 비효율적) 무효화 영역만 다시 그려주게 된다. 따라서 Windows는 이에 대한 복구를 위해 WM_PAINT 메시지의 Flag 설정을 해서 무효화 영역에 대한 처리를 하도록 한다. WM_PAINT 메시지 처리 루틴은 무효화 영역이 생길 경우를 미리 예측해서 이에 대한 복구 작업 루틴을 사용자가 직접 메시지 처리 루틴에서 정의해줘야 한다.

- MFC dialog 기반으로 생성한 MFC 프로그램을 생성하면, 기본적으로 아래와 같은 OnPaint함수가 선언되어 있는데, 내 생각엔 if 부분과 else  부분이 각각 초기 생성 시, 그리고 윈도우 무효화 영역 복구 시 루틴을 구현하는 부분이라고 생각했는데

 

- if문프로그램이 최소화 되었을 때 아이콘을 그려 주는 루틴이고, else 문의 경우 프로그램이 최소화가 아닌 경우 사용자가 처리해주어야 하는 무효화되었던 영역을 복구하는 루틴이다.

 

if문에 CPaintDC 객체 파괴자와 else문에 CDialog::OnPaint( ) 는 1로 설정된 WM_PAINT 메시지를 0으로 다시 변경해준다. 이게 무슨말이냐면! WM_PAINT 메시지는 Flag성 메시지라서 메시지가 발생해서 WM_PAINT 관련 플래그에 1로 설정되면, 메시지 큐가 비어있을 경우 WM_PAINT 메시지가 0으로 바뀌기 전까지 지속적으로 발생하게 된다.

 

따라서 우리가 무효화 영역을 복구시켜주는 루틴을 else에 선언하는데, else에 있는 CDialog::OnPaint() 함수를 없애버리면 WM_PAINT 메시지가 0으로 변경되지 않고 계속 1을 유지하게 된다. 이 말은, 지속적으로 다이얼로그의 OnPaint( )함수가 호출된다는 뜻이기 때문에 cpu 점유율을 증가시키게 된다.

( 만약 CDialog::OnPaint 호출 부를 없애고 싶다면, CPaintDC 객체 파괴자를 선언하면 된다.

-> else에 CPaintDC 객체 파괴자는 CDialog::OnPaint와 동일한 기능을 수행하기 때문에 중복으로 선언하지 말아야 한다.)

 

 

 

// 대화 상자에 최소화 단추를 추가할 경우 아이콘을 그리려면
//  아래 코드가 필요합니다. 문서/뷰 모델을 사용하는 MFC 응용 프로그램의 경우에는
//  프레임워크에서 이 작업을 자동으로 수행합니다.

void CMyPaint2Dlg::OnPaint()
{
    if (IsIconic())
    {
           CPaintDC dc(this); // 그리기를 위한 디바이스 컨텍스트

      SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

      // 클라이언트 사각형에서 아이콘을 가운데에 맞춥니다.
      int cxIcon = GetSystemMetrics(SM_CXICON);
      int cyIcon = GetSystemMetrics(SM_CYICON);
      CRect rect;
      GetClientRect(&rect);
      int x = (rect.Width() - cxIcon + 1) / 2;
      int y = (rect.Height() - cyIcon + 1) / 2;

      // 아이콘을 그립니다.
      dc.DrawIcon(x, y, m_hIcon);
}

else
{

      CDialog::OnPaint();
}

}

         

 

( 출처 http://www.tipssoft.com/bulletin/board.php?bo_table=FAQ&wr_id=636 )

 

 

 

 

 


 

 

4. 도형 찍기 기능 추가

  • 화면에 마우스 우클릭했을 때, 마우스 포인트에 도형이 찍히게 하는 기능
    -> OnLButtonDown (UINT nFlags, CPoint point) 함수에 구현
  • 삼각형

CClientDC dc(this);
POINT arPoint[] = {point.x, point.y, point.x-40, point.y+45, point.x+40, point.y+45 };
dc.Polygon(arPoint,3);

  • 사각형
dc.Rectangle(point.x-25,point.y-25,point.x+25,point.y+25);


dc.Ellipse(point.x-25,point.y-25,point.x+25,point.y+25);

 

 

5. 다이얼로그 배경 색 하얀색으로 변경

  • 다이얼로그 메세지에서 WM_ERASEBKGND 함수를 추가한 후, 생성된 OnEraseBkgnd(CDC* pDC) 함수에 다음 코드 작성

BOOL CMyPaint2Dlg::OnEraseBkgnd(CDC* pDC)
{
     // TODO: 여기에 메시지 처리기 코드를 추가 및/또는 기본값을 호출합니다.
     CRect rect;
     GetClientRect(rect);
     pDC->FillSolidRect( rect, RGB(255,255,255) );
     return TRUE;

}