Scrolling the Page

I saved this topic for last because it involves a few tricks. To be honest, it took me about six working days to figure out Patron's scrolling code. I hope this experience can save you some time because, on the surface, scrolling seems simple enough. If the mouse is held in the inset region (inside the edge of the window, not the droppable region) for the right amount of time, the target starts combining DROPEFFECT_SCROLL with any other effect flag. (This tells the user that scrolling will begin shortly.) When the mouse has stayed in the inset region for the set time, the target starts scrolling its window in the appropriate direction, using the DragOver pulse to continue scrolling if the mouse doesn't move. Inside DragOver, you scroll only if the scroll repeat rate time has elapsed since the last time you scrolled.

Patron first reads the DragScrollInset value from WIN.INI in the CPages constructor:


m_uScrollInset=GetProfileInt(TEXT("windows"), TEXT("DragScrollInset")
, DD_DEFSCROLLINSET);

CPages::UTestDroppablePoint uses m_uScrollInset to check whether the mouse coordinates are within the inset region of the pages window:


//In CPages::UTestDroppablePoint
UINT uRet;
RECT rcC;

GetClientRect(m_hWnd, &rcC);

[Code to store UDROP_NONE or UDROP_CLIENT in uRet]

//Scroll checks happen on client area.
if (PtInRect(&rcC, pt))
{
//Check horizontal inset.
if (pt.x <= rcC.left+(int)m_uScrollInset)
uRet œ= UDROP_INSETLEFT;
else if (pt.x >= rcC.right-(int)m_uScrollInset)
uRet œ= UDROP_INSETRIGHT;

//Check vertical inset.
if (pt.y <= rcC.top+(int)m_uScrollInset)
uRet œ= UDROP_INSETTOP;
else if (pt.y >= rcC.bottom-(int)m_uScrollInset)
uRet œ= UDROP_INSETBOTTOM;
}

UDROP_INSETLEFT and UDROP_INSETRIGHT are inclusive with both UDROP_INSETTOP and UDROP_INSETBOTTOM, but LEFT is mutually exclusive with RIGHT and TOP is mutually exclusive with BOTTOM. This means that Patron can scroll horizontally and vertically at the same time if the mouse is within both the horizontal and vertical inset regions. Patron will not, however, attempt to scroll up and down or left and right at the same time. That would be an interesting sight!

As mentioned earlier, UTestDroppablePoint is called each time in CDropTarget::DragEnter, CDropTarget::DragOver, and CDropTarget::Drop. The important call is the one in DragOver, in which the variable uRet contains the current UDROP_* combination and m_uLastTest (in CPages) contains the code from the last cycle through DragOver (or from DragEnter). So in any pass through DragOver, we know whether the cursor was outside the inset region and moved in, whether it was in the inset region and moved out, or whether we haven't changed from the last pass in or out of the region.

But CDropTarget has to be sensitive to the initial scroll delay so as to allow the user to move the mouse over the edge of the window without causing a scroll. This value is first loaded in CPages::CPages:


m_uScrollDelay=GetProfileInt(TEXT("windows"), TEXT("DragScrollDelay")
, DD_DEFSCROLLDELAY);

The default value for DD_DEFSCROLLDELAY is defined as 50 ms. Unfortunately, this is shorter than the 55-ms resolution of the Windows timer. It's a little hard to test a delay this short—create a DragScrollDelay=300 in the [windows] section of WIN.INI for testing purposes; this lets you move through things more slowly and test your timer counting.

Now we have to count the time from the moment the mouse entered the inset region in each iteration through DragOver. The best way to do this is with GetTickCount. A Windows timer does work because a timer requires you to be in your message loop calling GetMessage and DispatchMessage. But DoDragDrop does this only for mouse and keyboard messages. I tried using timers for implementing scrolling, but because my message loop never had a chance to run and dispatch WM_TIMER (or have DispatchMessage call a timer callback function), I never saw the timer expire.

Instead, DragOver saves in m_dwTimeLast the value from GetTickCount when the mouse first moved into the inset region. On every later call to DragOver, GetTickCount is called again and m_dwTimeLast is subtracted from it. If this difference is greater than the scroll rate, Patron scrolls a little and stores the current time in m_dwTimeLast. Through more calls to DragOver, Patron counts beyond the scroll rate again, scrolls a little more, resets as the base counter, and continues. If the mouse moves out of the region, Patron, of course, stops scrolling. To indicate this condition, DragOver sets m_dwTimeLast to 0(meaning "no scroll under any circumstances").

All of this is wrapped up in some repetitive-looking code in DragOver, in which uLast is the value in m_uLastTest and ppg is the current CPages pointer:


if ((UDROP_INSETHORZ & uLast) && !(UDROP_INSETHORZ & uRet))
ppg->m_uHScrollCode=0xFFFF;

if (!(UDROP_INSETHORZ & uLast) && (UDROP_INSETHORZ & uRet))
{
ppg->m_dwTimeLast=GetTickCount();
ppg->m_uHScrollCode=(0!=(UDROP_INSETLEFT & uRet))
? SB_LINELEFT : SB_LINERIGHT; //Same as UP and DOWN codes
}

if ((UDROP_INSETVERT & uLast) && !(UDROP_INSETVERT & uRet))
ppg->m_uVScrollCode=0xFFFF;

if (!(UDROP_INSETVERT & uLast) && (UDROP_INSETVERT & uRet))
{
ppg->m_dwTimeLast=GetTickCount();
ppg->m_uVScrollCode=(0!=(UDROP_INSETTOP & uRet))
? SB_LINEUP : SB_LINEDOWN;
}

if (0xFFFF==ppg->m_uHScrollCode && 0xFFFF==ppg->m_uVScrollCode)
ppg->m_dwTimeLast=0L;

//Set the scroll effect on any inset hit.
if ((UDROP_INSETHORZ œ UDROP_INSETVERT) & uRet)
*pdwEffect œ= DROPEFFECT_SCROLL;

This block of code checks for a change in the mouse's position relative to the inset region—in to out or out to in. It then sets up the CPages variables m_uHScrollCode and m_uVScrollCode with the codes to send with WM_HSCROLL and WM_VSCROLL messages, in which 0xFFFF is a flag that means "no scrolling."

DragOver then checks for expiration of the scroll rate timer. If the timer has elapsed, DragOver sends the appropriate scroll messages as follows:


if (ppg->m_dwTimeLast!=0
&& (GetTickCount()-ppg->m_dwTimeLast) > (DWORD)ppg->m_uScrollDelay)
{
if (0xFFFF!=ppg->m_uHScrollCode)
{
m_fPendingRepaint=TRUE;
SendMessage(ppg->m_hWnd, WM_HSCROLL, ppg->m_uHScrollCode, 0L);
}

if (0xFFFF!=ppg->m_uVScrollCode)
{
m_fPendingRepaint=TRUE;
SendMessage(ppg->m_hWnd, WM_VSCROLL, ppg->m_uVScrollCode, 0L);
}
}

This will send both WM_HSCROLL and WM_VSCROLL messages in the same pass through DragOver if necessary. This brings us to repainting. In Patron, drag-and-drop scrolling should be fast and should not require a repaint on every scroll. With many tenants on a page, especially tenants with bitmaps, each scroll would be painfully slow. To prevent the repaints, the m_fPendingRepaint flag is set to FALSE unless a scroll has occurred, in which case, it's set to TRUE. This flag is used in DragOver, DragLeave, and Drop to repaint the page when scrolling has stopped. The last two cases are obvious: moving out of the window or dropping stops scrolling. In DragOver, however, we have to determine whether the last SendMessage did, in fact, change the scroll position of the page. Therefore, before executing the preceding code, we save the current scroll positions in local variables:


xPos=ppg->m_xPos;
yPos=ppg->m_yPos;

After we have possibly sent WM_*SCROLL messages, we check the previous scroll positions against the new ones. If they are the same and a repaint is pending, we repaint:


if (xPos==ppg->m_xPos && yPos==ppg->m_yPos && m_fPendingRepaint)
{
UpdateWindow(ppg->m_hWnd);
m_fPendingRepaint=FALSE;
}

DragLeave and Drop always call UpdateWindow if the m_fPendingRepaint flag is TRUE.

One last consideration caused me much consternation. When I first implemented this scrolling business on a 16-bit platform, I tested it mostly by moving tenants around on the same page or among different documents. Everything worked great. Then I tried to drag something in from another application, such as Cosmo. Things fell apart because, with remote calls going on, my message loop had a chance to run. This didn't happen before because there's no yielding when both the source and the target are the same application!

The result was that Patron would occasionally receive WM_PAINT messages because scrolling, of course, invalidates regions of my client area. Normally this would not have been a problem except for my little end-user feedback rectangle. This is what happened: my CDropTarget::DragOver was called, I removed the previous feedback rectangle, I scrolled the page, and then I drew the new feedback rectangle over a possibly invalid region of the window. When WM_PAINT came along, it repainted that invalid region, erasing parts of my feedback rectangle. Then I came back into DragOver and attempted to erase the old feedback rectangle again. Because part of it was already gone and because my rectangle drawing is based on an XOR, I ended up with rectangle fragments on the screen. U-G-L-Y. I tried a number of things—ignoring the WM_PAINT messages, for example (which didn't work at all, as I should have known)—and finally arrived at a solution after a few more days of going nowhere. I maintain a flag in CPages::m_fDragRectShown that is modified only in DrawDropTargetRect: the flag is TRUE if the rectangle is visible, FALSE otherwise. If this flag is set when PagesWndProc in PAGEWIN.CPP is processing WM_PAINT, I call DrawDropTargetRect to erase the current rectangle, do the painting as usual, and then call DrawDropTargetRect again to reinstate the feedback. Finally everything came out clean. Yes, we all struggle at times with some aspects of programming for Windows!