윈도우 프로그래밍 팁: 마우스 Drag 구현 시

아래와 같이 몹시 Visual Studio의 Form Editor와 같은 프로그램이 있다고 하자. 여기서 버튼을 마우스로 클릭을 하고 드래그를 해서 이동하는 코드를 만들고 싶다고 하자.

일단 가장 간단하게 떠오를 수 있는 방법은:

  1. WM_LBUTTONDOWN 핸들러에서 아이템이 선택되어있고 그 위에서 힛이 일어났다는 걸 변수에 기록한다.
  2. WM_MOUSEMOVE에서 이 변수가 켜져있으면 드래그를 처리한다.
  3. WM_LBUTTONUP이 불리면 정식으로 움직임이 속성 값에 반영되도록 한다.
  4. ESC가 들어오면 중간에 취소가 되도록 한다.

정도가 될 것이다. 그런데 여기에서 부족한 점이 하나 있다. 바로 Drag Threshold를 고려하지 않았다는 점이다.

이걸 첨 들어보셨다면 한 번 바탕화면에 있는 아이콘 하나를 드래그를 해보기를 바란다. 마우스를 조심스래 클릭하고 아주 작은 움직임, 한두 픽셀 정도를 살짝 움직여 봐라. 아마 아무런 반응이 없을 것이다. 여기서 다시 마우스를 버튼을 놓으면 어떠한 드래그도 일어나지 않는다. 드래그를 하기 위해서는 마우스 왼쪽 버튼을 클릭한 채로 움직임이 최소 몇 픽셀 이상 되어야만 한다. 마치 물리학 책에 나오는 최대정지마찰력과 같은 개념이다.

이건 사용자의 실수를 막기 위해 둔 장치이다. 위의 방법은 어떤 문제가 있냐면, 사용자가 해당 컨트롤를 클릭만 하고 1~2픽셀만 움직여도 그만 컨트롤이 움직이는 일이 벌어진다는 것이다. 이걸 그대로 두면 상당히 짜증난다. 따라서 드래그를 구현할 때는 반드시 이런 문턱 값을 넘을 때 까지는 드래그를 진행해서는 안된다.

다시 위의 그림에서 컨트롤 드래그 방법을 생각해보자. 사실 나는 위의 각 메세지 핸들러에 드래그 관련 코드를 쭉 분산 시키는 것을 좋아하지 않는다.내가 좋아하는 방법은 일반적인 OLE Drag & Drop을 바로 이용하는 것이다. 그러면 Ctrl + 드래그&드랍을 바로 카피로 구현할 수도 있고 편해진다. MFC를 이용하면 COleDataSource::DoDragDrop를 이용해서 처리한다.

실제 이 COleDataSource (C:\Program Files\Microsoft Visual Studio 8\VC\atlmfc\src\mfc\oledrop1.cpp)의 OnBeginDrag 메소드에는 언급한 drag threshold를 처리하고 있다.

BOOL COleDropSource::OnBeginDrag(CWnd* pWnd)
{
ASSERT_VALID(this);

m_bDragStarted = FALSE;

// opposite button cancels drag operation
m_dwButtonCancel = 0;
m_dwButtonDrop = 0;
if (GetKeyState(VK_LBUTTON) < 0)
{
m_dwButtonDrop |= MK_LBUTTON;
m_dwButtonCancel |= MK_RBUTTON;
}
else if (GetKeyState(VK_RBUTTON) < 0)
{
m_dwButtonDrop |= MK_RBUTTON;
m_dwButtonCancel |= MK_LBUTTON;
}

DWORD dwLastTick = GetTickCount();
pWnd->SetCapture();

while (!m_bDragStarted)
{
// some applications steal capture away at random times
if (CWnd::GetCapture() != pWnd)
break;

// peek for next input message
MSG msg;
if (PeekMessage(&msg, NULL, WM_MOUSEFIRST, AFX_WM_MOUSELAST, PM_REMOVE) ||
PeekMessage(&msg, NULL, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE))
{
// check for button cancellation (any button down will cancel)
if (msg.message == WM_LBUTTONUP || msg.message == WM_RBUTTONUP ||
msg.message == WM_LBUTTONDOWN || msg.message == WM_RBUTTONDOWN)
break;

// check for keyboard cancellation
if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)
break;

// check for drag start transition
m_bDragStarted = !m_rectStartDrag.PtInRect(msg.pt);
}

// if the user sits here long enough, we eventually start the drag
if (GetTickCount() - dwLastTick > nDragDelay)
m_bDragStarted = TRUE;
}
ReleaseCapture();

return m_bDragStarted;
}

코드에서 보다시피 메세지 큐를 계속 읽어와서 마우스 움직임이 m_rectStartDrag에 정의된 값 보다 크다면 (예를 들어, x/y 방향 중 어느 한 쪽이라도 4픽셀보다 크게 움직인다면) drag가 시작되었다고 판단한다. 또, 특정 시간 이후가 지나도 드래그로 인식하도록 하고 있다.

요렇게 간단히 드래그에도 이런 복잡함이 숨어있다는 사실. UI 프로그래밍은 이렇게 미묘한 것이 쌓여 엄청난 차이를 주기 때문에 이런 미묘한 것도 주의 깊게 관찰하는 센스가 필요하다.

p.s. 또 하나 이런 드래그 코드를 두는 방법은 드래그 시작이 감지되었을 때 자체 메세지 루프를 돌리는 것이다. 이런 경우 관련 코드를 한 곳에 모을 수 있고 불필요한 클래스 변수를 하나 없앨 수 있는 장점이 있다.

// OnBeginDrag 코드를 대략 카피해서 만든 녀석
if (!CheckDragStarted())
return;

AfxLockTempMaps();
SetCapture();

for (;;)
{
MSG msg;
VERIFY(::GetMessage(&msg, NULL, 0, 0));

if (::GetCapture() != m_hWnd)
break;

switch (msg.message)
{
case WM_LBUTTONUP:
case WM_MOUSEMOVE:
// 처리...
break;

case WM_KEYDOWN:
if (msg.wParam != VK_ESCAPE)
break;

case WM_RBUTTONDOWN:
// 님아 goto 써서 죄송
goto ExitLoop;

default:
DispatchMessage(&msg);
break;
}
}

ExitLoop:
ReleaseCapture();
AfxUnlockTempMaps(FALSE);
by object | 2008/03/19 07:20 | 컴퓨터 | 트랙백 | 덧글(11)
트랙백 주소 : http://minjang.egloos.com/tb/1803285
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by eslife at 2008/03/20 19:09
Threshold 란 단어가 그런 뜻이었군요 .
예전 학부때, 원서를 보다가 Thread 란 단어를 보고 뭔 말인지 몰라서, 교수님을 찾았다가 교수님도 모르시는 지라 크게 좌절한 적이 있었습니다 ..ㅋㅋ 그래서 졸업할때까지 Thread 가 '실' 인줄 알았었는데..
Threshold 를 보니 갑자기 옛날 생각이 나네요.. 좋은 글에 엄한 댓글이었습니다 :)
Commented by 수아기 at 2008/03/21 21:11
eslife님의 교수님께서 그걸 모르셨다니 좀 놀라운걸요.ㅠ.ㅠ
Threshold 문턱치라고 많이 번역들을 해 놓으셨죠.^^
좋은 포스팅 잘 읽었습니다.
Commented by object at 2008/03/22 02:46
다른 말로는... 역치값 정도로 표현할 수 있겠죠. 생물에서 배웠던... 베버법칙과 함께 나오기도..
Commented by XJeongseok at 2008/03/25 08:38
민장이형, 형이 전에 이야기 하셨던 드래그에 대한 팁이 이것이었군요!
지금 딱 그걸 구현해야하는데, 잘 보고 갑니다.
그런데 이글루스에는 스크랩 기능이 없나요? 아니면 트랙백으로 해야 하는 건지요.
이글루스는 물론이고 블로그는 처음 이용하는 거라 잘 모르겠네요. ㅎㅎ
저도 아침에 심심해서 블로그 하나 만들었습니다. -ㅅ-
Commented by XJeongseok at 2008/03/25 08:40
아, 저 정석이 입니다. ㅎㅎ;
Commented by object at 2008/03/25 09:23
X정석이라..
Commented by XJeongseok at 2008/03/25 11:09
class XJeongseok : public XObject -_-;

창의력이 부족하다보니, 딱히 ID로 할 것이 없더라구요 ㅎㅎ
Commented by daybreaker at 2008/03/26 21:40
요즘은 그러 세세한 UI 프로그래밍을 별로 해보지 않았지만 예전에 VB로 자체 버튼·메뉴 컨트롤 구현하던 생각이 나는군요. UI는 정말 세세한 차이가 퀄리티 차이를 팍 나게 만들죠.
그나저나 자체 메시지루프 돌려버리는 것도 아이디어 좋은데요?
Commented by 박지예 at 2008/04/23 19:34
안녕하세요..
어느글에다가 글을쓸까 막막 고민했네요.. 음... 구글리더로 확 읽어서 님의 글을 늘 지켜보고(^^;)잇답니다.
이번에 회사서 좀 가벼운 파워포인트 유사한 프로그램을 만들어야 한답니다...
참고로 여긴 분야가 제조쪽이라 플그래머가 저뿐이에요. 여기저기 뒤적거리면서 혼자 작업하는데요..
mfc 즐겨 사용하고.. 좀 무식하게 프로그래밍 하는데 이 사이트에서 반성 좀 하게 되었어요^^
6.0 쓰다가 님 글읽고 느낀게 잇어서 2005로 갈아타려고 오늘에서야 깔았습니다^^;
그 기념(?)으로 책 하나 사려고 하는데...
무식하지 않게 프로그래밍 할 수 있게 추천 해 주실래요? 음...
님이 작년에 연재하신 부분에서 엔진!을 구현하는데 도움을 받을 수 있는 책 정도면 좋을것 같습니다...
댓글도 좋고(자주자주 들어와서 보고 있으니까요 ㅎㅎ)
메일로 조언 주셔도 좋구요.... shsw012@gmail.com
...... ^///^;;
Commented by object at 2008/04/23 19:57
저 위에 보면 제 메일 주소가 있습니다. 거기로 연락 주시면 되겠습니다.

네, 제가 만든 일이 파워포인트같은 툴을 만드는 일이었는데요. 책을 본 적은 없고 제가 다른 툴을 분석하고 그냥 몇 년 동안의 시행 착오를 겪은 끝에 만든 것입니다. 그런데 다른 유명한 툴들도 다 그렇게 하고 있습니다. 예전에 좀 썼던 엔진 관련 글이 그 중 일부인데... 댓글로 적기에는 좀 벅차네요. 메일로 쓰기도 벅차고;; 간단하게 만들거면 그냥 대충 만들어도 되고 제대로 만들려면 좀 일을 벌리셔야 합니다.

책이라고 하면 굳이 디자인 패턴을 들 수 있는데, 전 디자인 패턴 책을 굉장히 싫어합니다. 사람들을 보면 프로그램은 짜보지도 않고 패턴만 외우고 정작 프로그램 짤 때 그 패턴에 갇혀 삽질하는 것을 많이 봅니다. 그냥 자유롭게 생각해보세요. 그리고 그걸 더 일반화 더 최적화 시키다보면 결국 디자인 패턴이 말하는 것으로 귀결 될 수 밖에 없더군요. 음.. 그냥 생각 많이 해보시라는 말 밖에는 일단...
Commented by 박지예 at 2008/04/24 16:05
작업을 하다보니 결국 하지말라!고 하신 방향으로 가고 있더군요.
생각..많이해보고 있는데 답답한 부분이 많네요. 윈도우 메세지에 코드 다 때려박아놓은거 다 지우고 제대로 해보렵니다^^;
이론적인 뒷바침이 필요할 것 같습니다. 아는만큼 보인다잖아요^^
음.. 디자인 패턴보다 짬이 날 때 MFC 구조,원리를 살펴보는게 낫겠죠? 그런 류의 책을 본것 같기도 하고요.
님이 그 경지까지 가기까지 힘드셨 듯.. 화이팅해야겟습니다^^ 도움 말씀 감사해요.

:         :

:

비공개 덧글

<< 이전 페이지 다음 페이지 >>





by object 여기는 공사중....
최근 등록된 덧글
최근 등록된 트랙백
VisualStudio 2005에서 Gui..
by 셈말짓기
SSD와 WD의 벨로시랩터
by 정보와 휴식...그리고 미래

한RSS 구독자수 website counter

한RSS에 추가

Add to Google

rss

skin by 이글루스