VC상에서 MOUSEOVER기능?

MFC 2019. 3. 10. 21:18 Posted by 쫀다
트랙백 주소 : http://www.tipssoft.com/bulletin/tb.php/QnA/14907
화면버튼에 마우스를 가져가면 클릭하지 않더라도 이미지를 변경하도록 하려고 합니다.
 
Static은 Mousemove 이벤트를 통해서 구현가능한데
 
버튼은 Mousemove이벤트가 버튼에 올라가면 발생안하더군요
 
해결할 방법이 없는지요? 2005등에서는 이벤트가 있는거 같은데 vc6.0에서는 없어서요?
 
조언 부탁드립니다.


관리자 09-08-12 14:18
 
버튼도 윈도우이기 때문에 마우스 이동 메시지가 버튼위에서 발생하면 버튼으로 메시지가 
들어가기 때문에 베이스윈도우에는 전달되지 않아서 그럴겁니다. 이런문제를 해결하는 방법은 
보통 두가지로 해결하는데, 

첫번째 방법은 베이스윈도우의 PreTranslateMessage 함수를 등록하여 이 함수에서 
WM_MOUSEMOVE를 체크하도록 하면 컨트롤로 전달되는 메시지를 중간에 다 확인 가능하기 
때문에 원하는 기능을 구현할수 있을겁니다. 

두번째 방법은 CButton( 또는 CBitmapButton ) 에서 계승을 받은 후에 WM_MOUSEMOVE 메시지를 
등록하고 원하는 기능을 처리한후에 서브클래싱을 사용하여 해당 컨트롤에 자신이 만든 
클래스를 사용하도록 변경하면됩니다. ^^;;
까망장어 09-08-12 17:58
 
감사합니다. 결국 두번째 방법으로 해결했습니다.

Subclassing으로 Edit의 기능 확장하기

MFC 2019. 3. 10. 18:52 Posted by 쫀다


<출처> https://blog.naver.com/tipsware/221163757179



:   Win32 프로그래밍 관련 전체 목차
http://blog.naver.com/tipsware/221059977193

1. 작업 개요
이 글에서는 채팅을 위한 사용자 환경을 구성하는 예제를 만들어 보겠습니다. 아래와 같이 화면을 구성하고 아래쪽에 만들어진 Edit 컨트롤에 글을 입력하고 엔터키를 누르면 Edit 컨트롤에 쓰인 내용이 ListBox에 추가되는 예제입니다.

그리고 아래의 동영상은 이 글에서 설명하고자 하는 내용을 요약한 것입니다. 이 영상을 먼저 보는 것이 이 강좌를 이해하는데 도움이 많이 될 것입니다.

  

2. 기본 환경 만들기
먼저 메인 윈도우에 ListBox와 Edit 컨트롤을 생성하여 이 예제의 기본 틀을 구성하겠습니다. 메인 윈도우는 TW_Wnd 클래스를 사용했고 ListBox는 TW_ListBoxWnd 클래스를 사용했고 Edit 컨트롤은 아직 클래스화를 하지 않아서 직접 생성했습니다. 그리고 글꼴은 ListBox와 Edit 컨트롤에 둘 다 사용해야 해서 변수를  추가로 선언해서 관리했습니다.

class MyWnd : public TW_Wnd { private: TW_ListBoxWnd m_chat_list; // 채팅 목록을 관리할 ListBox 객체 HFONT mh_font; // 컨트롤이 사용할 글꼴 HWND mh_chat_edit; // 채팅을 입력할 Edit 컨트롤 public: MyWnd() // 객체 생성자 { } virtual int OnCreate(CREATESTRUCT *ap_create_info) { TW_Wnd::OnCreate(ap_create_info); // '굴림 - 12 크기'로 글꼴을 생성! mh_font = TWAPI_CreateFont(); // 시작점이 (3, 3)이고 끝점이(421, 217)이고 ID가 25001인 ListBox를 생성한다. m_chat_list.CreateEx(mh_wnd, 3, 3, 421, 190, 25001); // m_file_list에 생성된 글꼴을 설정한다. m_chat_list.SetFont(TWAPI_CreateFont(), 0); // ListBox 아래에 Edit 컨트롤을 생성한다. mh_chat_edit = ::CreateWindow(L"EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 3, 193, 418, 22, mh_wnd, (HMENU)25002, NULL, NULL); // mh_chat_edit에 생성된 글꼴을 설정한다. ::SendMessage(mh_chat_edit, WM_SETFONT, (WPARAM)mh_font, 1); return 0; } virtual void OnPaint() { // 부모 Window에 WM_PAINT 메시지가 발생하면 자식 Window에도 // WM_PAINT 메시지를 전달해야 한다. m_chat_list.Invalidate(); ::InvalidateRect(mh_chat_edit, NULL, TRUE); mp_target->BeginDraw(); // 파란색으로 클라이언트 영역을 채운다. mp_target->Clear(ColorF(0.1875f, 0.2343f, 0.3203f)); mp_target->EndDraw(); } virtual void OnDestroy() { TW_Wnd::OnDestroy(); ::DestroyWindow(mh_chat_edit); // Edit 컨트롤을 제거한다. ::DeleteObject(mh_font); // 글꼴을 제거한다. } };

에디트 컨트롤을 직접 생성하는 방법에 대해서 잘 모른다면 아래의 글을 참고하기 바랍니다.

3. Edit 컨트롤의 기능 확장하기
Edit 컨트롤에 글을 적고 엔터키를 누르면 Edit 컨트롤에 적힌 글을 읽어서 ListBox에 추가해야 합니다. 그런데 이 작업을 하려면 Edit 컨트롤에서 엔터키를 눌렀다는 것을 체크해야 하는데 Edit 컨트롤은 해당 상황을 메인 윈도우에 알려주지 않습니다.

아마도 Windows 운영체제를 만든 사람들은 이런 상황에서는 Button 컨트롤을 사용해야지 Button 컨트롤의 역할이 분명해지기 때문에 Edit 컨트롤에서 엔터키를 누른 상황을 Notify 메시지로 추가하지 않았을 거라고 생각됩니다.

따라서 위 그림에서 Edit 컨트롤의 크기를 조금 줄이고 Button을 추가해서 사용자에게 'Edit에 적은 글을 ListBox에 추가하려면 이 Button을 누르세요!'라고 설명하는 것이 권장 사항이라는 뜻입니다. 사실 이런 사용자 환경이 좀 더 직관적이긴 합니다.

하지만 사용자 입장에서 봤을 때 Edit에 글을 입력하고 ListBox에 추가하기 위해서 마우스로 버튼을 클릭하는 것이 편할까요? 아니면 그냥 Edit에 글을 입력하고 엔터키를 누르는 게 편할까요? 아무래도 손을 적게 움직이는 엔터키를 누르는 방법이 더 편할 것입니다. (물론 두 가지 방법을 모두 제공하는 것이 가장 좋지만 지금은 기능 비교를 위해서 이렇게 설명하는 것입니다.)

그런데 문제는 이렇게 구현하고 싶어도 Edit 컨트롤이 엔터키를 눌렀다는 상황을 메인 윈도우로 알려주지 않기 때문에 구현하고 싶어도 할 수 없다는 것입니다. 그렇다고 이 기능 때문에 Edit 컨트롤을 직접 구현하는 것은 너무 힘든 작업입니다. 직접 해보면 알겠지만 Edit 컨트롤을 만드는 작업은 보기와 달리 정말 많은 작업을 요구합니다. 저는 컨트롤을 직접 만들어서 사용하는 경우가 많아서 정말 많은 컨트롤을 만들어봤는데 가장 힘들었던 작업이 Edit 컨트롤을 만드는 작업이었습니다.

[ 윈도우 프러시저 교체하기 ]
그래서 Edit 컨트롤을 직접 만들지 않고 Subclassing 기술을 사용해서 Edit 컨트롤에 제가 원하는 기능만 추가하도록 하겠습니다. Edit 컨트롤도 윈도우이기 때문에 윈도우 프러시저를 가지고 있습니다. 그리고 이 프러시저는 아래와 같이 SetWindowLong 함수를 통해서 외부에서 변경이 가능합니다. 즉, Edit를 위한 프러시저를 내가 직접 만들어서 Edit가 사용하던 프러시저와 교체를 할 수 있다는 뜻입니다. 

// Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { return 0; } class MyWnd : public TW_Wnd { // ... 나머지 코드 생략 ... virtual int OnCreate(CREATESTRUCT *ap_create_info) { TW_Wnd::OnCreate(ap_create_info); // ... 나머지 코드 생략 ... mh_chat_edit = ::CreateWindow(L"EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 3, 193, 418, 22, mh_wnd, (HMENU)25002, NULL, NULL); // mh_chat_edit에 생성된 글꼴을 설정한다. ::SendMessage(mh_chat_edit, WM_SETFONT, (WPARAM)mh_font, 1); // Edit 컨트롤에 내가 만든 프로시저를 설정! SetWindowLong(mh_chat_edit, GWL_WNDPROC, (LONG)MyEditProc); return 0; } };

하지만 위와 같이 작업하면 MyEditProc에 Edit 컨트롤을 위한 메시지 처리를 모두 직접 구현해야 합니다. 그래서 우리는 Edit 컨트롤이 사용하던 윈도우 프러시저를 재활용할 것입니다. 먼저 Edit 컨트롤이 기존에 사용하던 윈도우 프러시저는 SetWindowLong 함수의 반환값으로 얻을 수 있습니다.

// Edit 컨트롤이 본래 사용하던 프로시저의 주소를 저장하기 위한 함수의 포인터 WNDPROC g_org_edit_proc; // Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { return 0; } class MyWnd : public TW_Wnd { // ... 나머지 코드 생략 ... virtual int OnCreate(CREATESTRUCT *ap_create_info) { TW_Wnd::OnCreate(ap_create_info); // ... 나머지 코드 생략 ... // Edit 컨트롤에 내가 만든 프로시저를 설정하고 Edit 컨트롤이 기존에 사용하던 // 프로시저의 주소를 g_org_edit_proc에 저장한다. g_org_edit_proc = (WNDPROC)SetWindowLong(mh_chat_edit, GWL_WNDPROC, (LONG)MyEditProc); return 0; } };

그리고 이렇게 얻은 g_org_edit_proc를 MyEditProc 함수에서 아래와 같이 CallWindowProc 함수를 사용하여 호출합니다. 이렇게 하면 MyEditProc는 기존 Edit 컨트롤의 프러시저를 그대로 사용하기 때문에 기존 Edit 컨트롤과 동일한 기능을 수행하게 됩니다.

// Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { // 위 상황이 아니라면 기존 Edit 컨트롤이 사용하던 프로시저를 호출하여 // 나머지 기능들을 수행합니다. return ::CallWindowProc(g_org_edit_proc,ah_wnd,message_id,wParam,lParam); }

그리고 이렇게 교체한 Edit 컨트롤의 프러시저는 프로그램이 종료할 때 아래와 같이 기존 프러시저로 복구 시켜주면 됩니다.

virtual void OnDestroy() { TW_Wnd::OnDestroy(); // 기존에 사용하던 Edit 컨트롤의 프로시저로 복구한다. SetWindowLong(mh_chat_edit, GWL_WNDPROC, (LONG)g_org_edit_proc); ::DestroyWindow(mh_chat_edit); // Edit 컨트롤을 제거한다. ::DeleteObject(mh_font); // 글꼴을 제거한다. }

[ Edit에 프러시저에 기능 추가하기 ]
이제 Edit 컨트롤에 사용할 새 윈도우 프러시저를 구성했으니 Edit 컨트롤에서 엔터키를 눌렀다가 해제할 때 부모(메인) 윈도우인 MyWnd로 Notify 메시지를 전달하도록 코드를 추가해보겠습니다. 먼저 엔터키가 눌러졌다가 해제되었는지 체크하기 위해서 아래와 같이 코드를 추가합니다.

// Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { // RETURN 키가 눌러졌다가 해제되었는지를 체크한다. if (message_id == WM_KEYUP && wParam == VK_RETURN) { } // 위 상황이 아니라면 기존 Edit 컨트롤이 사용하던 프로시저를 호출하여 // 나머지 기능들을 수행합니다. return ::CallWindowProc(g_org_edit_proc,ah_wnd,message_id,wParam,lParam); }

그리고 엔터키가 눌러졌다가 해제되었다면 WM_COMMAND 메시지를 사용하여 Notify code를 부모 윈도우로 전송하는 코드를 아래와 같이 추가합니다. WM_COMMAND 메시지를 사용하는 경우에는 wParam의 하위 16비트에 컨트롤의 ID를 기록하고 상위 16비트에 Notify code를 기록하는데 이 예제에서는 Edit 컨트롤의 ID가 25002번이고 Notify code는 1000번으로 사용했습니다. 그리고 lParam에는 컨트롤의 핸들 값을 적으면 됩니다.

// Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { // RETURN 키가 눌러졌다가 해제되었는지를 체크한다. if (message_id == WM_KEYUP && wParam == VK_RETURN) { // 부모 윈도우로 25002번 컨트롤에서 1000번 Notify code가 발생했음을 알려준다. // 1000번은 개발자가 임의로 설정한 코드입니다. return ::PostMessage(::GetParent(ah_wnd), WM_COMMAND, MAKEWPARAM(25002, 1000), (LPARAM)ah_wnd); } // 위 상황이 아니라면 기존 Edit 컨트롤이 사용하던 프로시저를 호출하여 // 나머지 기능들을 수행합니다. return ::CallWindowProc(g_org_edit_proc,ah_wnd,message_id,wParam,lParam); }

위에서 사용하는 1000이라고 사용한 Notify code 값은 제가 임의로 정한 값이고 다른 값을 사용해도 상관없습니다. 이제 이렇게 작업을 하고 나면 기존 Edit를 위한 작업이나 메시지는 그대로 사용할 수 있고 Edit 컨트롤에서 엔터키를 눌렀다가 해제하게 되면 부모 윈도우로 Notify code가 포함된 WM_COMMAND 메시지가 발생하게 됩니다.

따라서 MyWnd 클래스에 WM_COMMAND 메시지를 처리하는 OnCommand 함수를 추가하고 이 Notify 메시지를 처리하기 위한 코드를 아래와 같이 추가합니다.

class MyWnd : public TW_Wnd { // ... 나머지 코드 생략 ... virtual void OnCommand(INT32 a_ctrl_id, INT32 a_notify_code, HWND ah_ctrl) { if (a_ctrl_id == 25002) { // Edit에서 전달된 메시지인지 체크한다. if (a_notify_code == 1000) { // Edit에서 엔터키를 누른 경우 } } } // ... 나머지 코드 생략 ... };

따라서 1000번이라는 Notify code가 부모 윈도우로 전달되면 Edit 컨트롤에서 엔터키를 눌렀다는 뜻이기 때문에 아래와 같이 GetDlgItemText 함수로 Edit 컨트롤에 적혀있는 문자열을 가져와서 ListBox에 추가합니다. 그리고 Edit 컨트롤에 사용자가 바로 다음 글을 적을 수 있도록 기존에 적었던 글을 제거합니다.

virtual void OnCommand(INT32 a_ctrl_id, INT32 a_notify_code, HWND ah_ctrl) { if (a_ctrl_id == 25002) { // Edit에서 전달된 메시지인지 체크한다. if (a_notify_code == 1000) { // Edit에서 엔터키를 누른 경우 wchar_t str[128]; // Edit 컨트롤에 적힌 문자열을 가져온다. ::GetDlgItemText(mh_wnd, 25002, str, 128); // ListBox에 해당 문자열을 추가한다. m_chat_list.InsertString(-1, str); // Edit 컨트롤에 쓰여진 문자열을 제거한다. ::SetDlgItemText(mh_wnd, 25002, L""); } } }

이제 이렇게 작업하고 나면 위 동영상에서 본 것처럼 Edit 컨트롤에 글을 입력하고 엔터키를 누르면 ListBox에 해당 문자열이 추가될 것입니다.

4. 예제 전체 소스
지금까지 설명한 예제의 전체 소스는 다음과 같습니다.

#include "stdafx.h" #include "ExamSubclassWnd.h" #include "twapi_w01.h" #include "twapi_w01_gdi.h" #include "twapi_w01_util.h" #include "twapi_w01_socket.h" #pragma comment(lib, "D2D1.lib") #pragma comment(lib, "D3D11.lib") #pragma comment(lib, "DXGI.lib") #pragma comment(lib, "dxguid.lib") #pragma comment(lib, "DWRITE.lib") #pragma comment(lib, "Msimg32.lib") #pragma comment(lib, "WS2_32.lib") #ifdef _DEBUG #pragma comment(lib, "DSH_TWAPI_W01.lib") #else #pragma comment(lib, "RST_TWAPI_W01.lib") #endif // Edit 컨트롤이 본래 사용하던 프로시저의 주소를 저장하기 위한 함수의 포인터 WNDPROC g_org_edit_proc; // Edit 컨트롤에 새롭게 설정할 윈도우 프로시저 LRESULT APIENTRY MyEditProc(HWND ah_wnd, UINT message_id, WPARAM wParam, LPARAM lParam) { // RETURN 키가 눌러졌다가 해제되었는지를 체크한다. if (message_id == WM_KEYUP && wParam == VK_RETURN) { // 부모 윈도우로 25002번 컨트롤에서 1000번 Notify code가 발생했음을 알려준다. // 1000번은 개발자가 임의로 설정한 코드입니다. return ::PostMessage(::GetParent(ah_wnd), WM_COMMAND, MAKEWPARAM(25002, 1000), (LPARAM)ah_wnd); } // 위 상황이 아니라면 기존 Edit 컨트롤이 사용하던 프로시저를 호출하여 // 나머지 기능들을 수행합니다. return ::CallWindowProc(g_org_edit_proc,ah_wnd,message_id,wParam,lParam); } class MyWnd : public TW_Wnd { private: TW_ListBoxWnd m_chat_list; // 채팅 목록을 관리할 ListBox 객체 HFONT mh_font; // 컨트롤이 사용할 글꼴 HWND mh_chat_edit; // 채팅을 입력할 Edit 컨트롤 public: MyWnd() { } // 객체 생성자 virtual void OnCommand(INT32 a_ctrl_id, INT32 a_notify_code, HWND ah_ctrl) { if (a_ctrl_id == 25002) { // Edit에서 전달된 메시지인지 체크한다. if (a_notify_code == 1000) { // Edit에서 엔터키를 누른 경우 wchar_t str[128]; // Edit 컨트롤에 적힌 문자열을 가져온다. ::GetDlgItemText(mh_wnd, 25002, str, 128); // ListBox에 해당 문자열을 추가한다. m_chat_list.InsertString(-1, str); // Edit 컨트롤에 쓰여진 문자열을 제거한다. ::SetDlgItemText(mh_wnd, 25002, L""); } } } virtual int OnCreate(CREATESTRUCT *ap_create_info) { TW_Wnd::OnCreate(ap_create_info); // '굴림 - 12 크기'로 글꼴을 생성! mh_font = TWAPI_CreateFont(); // 시작점이 (3, 3), 끝점이(421, 190)이고 ID가 25001인 ListBox를 생성한다. m_chat_list.CreateEx(mh_wnd, 3, 3, 421, 190, 25001); // m_file_list에 생성된 글꼴을 설정한다. m_chat_list.SetFont(TWAPI_CreateFont(), 0); // ListBox 아래에 Edit 컨트롤을 생성한다. mh_chat_edit = ::CreateWindow(L"EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 3, 193, 418, 22, mh_wnd, (HMENU)25002, NULL, NULL); // mh_chat_edit에 생성된 글꼴을 설정한다. ::SendMessage(mh_chat_edit, WM_SETFONT, (WPARAM)mh_font, 1); // Edit 컨트롤에 내가 만든 프로시저를 설정하고 Edit 컨트롤이 기존에 사용하던 // 프로시저의 주소를 g_org_edit_proc에 저장한다. g_org_edit_proc = (WNDPROC)SetWindowLong(mh_chat_edit, GWL_WNDPROC, (LONG)MyEditProc); return 0; } virtual void OnPaint() { // 부모 Window에 WM_PAINT 메시지가 발생하면 자식 Window에도 // WM_PAINT 메시지를 전달해야 한다. m_chat_list.Invalidate(); ::InvalidateRect(mh_chat_edit, NULL, TRUE); mp_target->BeginDraw(); // 파란색으로 클라이언트 영역을 채운다. mp_target->Clear(ColorF(0.1875f, 0.2343f, 0.3203f)); mp_target->EndDraw(); } virtual void OnDestroy() { TW_Wnd::OnDestroy(); // 기존에 사용하던 Edit 컨트롤의 프로시저로 복구한다. SetWindowLong(mh_chat_edit, GWL_WNDPROC, (LONG)g_org_edit_proc); ::DestroyWindow(mh_chat_edit); // Edit 컨트롤을 제거한다. ::DeleteObject(mh_font); // 글꼴을 제거한다. } }; class MyApp : public TW_WinApp { public: MyApp(HINSTANCE ah_instance, const wchar_t *ap_class_name) : TW_WinApp(ah_instance, ap_class_name) { } virtual void InitInstance() { mp_wnd = new MyWnd; mp_wnd->Create(L"Exam SubclassWnd - Tipssoft.com", 50, 50, 440, 257); mp_wnd->ShowWindow(); mp_wnd->UpdateWindow(); } }; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MyApp tips_app(hInstance, L"ExamSubclassWnd"); return tips_app.NormalProcess(); }

아래의 첨부 파일은 위 예제 프로젝트를 압축한 것입니다. 위 예제는 VS2017로 만들어졌고 최신 TWAPI_W01 라이브러리를 포함하고 있습니다







다이얼로그 백그라운드 흰색으로 설정

MFC 2019. 3. 10. 13:00 Posted by 쫀다

C클래스Dlg의 WM_ERASEBKGND 메세지의 OnEraseBkgnd 추가


return CDialogEx::OnEraseBkgnd(pDC); 를 주석처리한다.



BOOL C클래스Dlg::OnEraseBkgnd(CDC* pDC)

{

// TODO: Add your message handler code here and/or call default


CRect Rect;

GetClientRect(&Rect);

pDC->FillSolidRect(&Rect, RGB(255, 255, 255));

return TRUE;


//return CDialogEx::OnEraseBkgnd(pDC);

}