A customer wanted to prevent the user from dragging an object into a specific region of their window. Their current implementation detects that the mouse is an in illegal location and uses SetÂCursorÂPos to move it to a nearby legal location. However, this creates flicker because the cursor actually does enter the illegal region and then jumps out.
Let’s illustrate this with our scratch program.
POINT g_pt;
const RECT g_rcExclude = { 100, 100, 200, 200 };
RECT ItemRect(POINT pt)
{
return RECT{ pt.x - 10, pt.y - 10, pt.x + 10, pt.y + 10 };
}
The g_pt variable holds the location of our object, and the g_rcExclude is the rectangle in which the object is forbidden. The ItemRect function produces a bounding rectangle for our object so we can draw something there.
void
PaintContent(HWND hwnd, PAINTSTRUCT* pps)
{
FillRect(pps->hdc, &g_rcExclude, (HBRUSH)(COLOR_WINDOWTEXT + 1));
RECT rcItem = ItemRect(g_pt);
FillRect(pps->hdc, &rcItem, (HBRUSH)(COLOR_APPWORKSPACE + 1));
}
Painting our content is a straightforward matter of drawing the forbidden rectangle in the text color and drawing the object in the app workspace color.
void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
POINT ptNew = { x, y };
if (PtInRect(&g_rcExclude, ptNew)) {
// Clamp to nearest legal position
int leftMargin = ptNew.x - g_rcExclude.left;
int topMargin = ptNew.y - g_rcExclude.top;
int rightMargin = g_rcExclude.right - ptNew.x;
int bottomMargin = g_rcExclude.bottom - ptNew.y;
int dx, dy;
int x, y;
if (leftMargin < rightMargin) {
x = g_rcExclude.left;
dx = leftMargin;
} else {
x = g_rcExclude.right;
dx = rightMargin;
}
if (topMargin < bottomMargin) {
y = g_rcExclude.top;
dy = topMargin;
} else {
y = g_rcExclude.bottom;
dy = bottomMargin;
}
if (dx < dy) {
ptNew.x = x;
} else {
ptNew.y = y;
}
POINT ptScreen = ptNew;
ClientToScreen(hwnd, &ptScreen);
SetCursorPos(ptScreen.x, ptScreen.y);
}
if (g_pt.x != ptNew.x || g_pt.y != ptNew.y) {
RECT rcItem = ItemRect(g_pt);
InvalidateRect(hwnd, &rcItem, TRUE);
g_pt = ptNew;
rcItem = ItemRect(g_pt);
InvalidateRect(hwnd, &rcItem, TRUE);
}
}
// Add to WndProc
HANDLE_MSG(hwnd, WM_MOUSEMOVE, OnMouseMove);
When the mouse moves, we take the mouse position and see if it is in the forbidden rectangle. If so, we update the coordinates to the nearest legal position and move the mouse there with SetÂCursorÂPos.
Whether or not we had to update the coordinates, if the result produces a new location, then invalidate the object’s old location (so it will be erased at the next paint cycle), update the object position, and then invalidate the object’s new position (so it will be drawn at the next paint cycle).
When you run this program, you can try to move the mouse into the forbidden rectangle, but the program will shove the mouse back out. However, it flickers a lot bcause the mouse briefly enters the forbidden rectangle before it is expelled from it.
The customer saw that there is a ClipÂCursor function to constrain the mouse to remain inside a rectangle, but is there an inverse version that forces the mouse to remain outside a rectangle?
There is no such function, but that’s okay.
What you can do when the mouse is in an illegal position is just pretend that it’s in a legal position. Let the user move the mouse into a illegal position, but show the feedback at the nearest legal position, and if they drop the object, let it drop at the nearest legal position.
In the above program, that means we remove the call to SetÂCursorÂPos.
void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
POINT ptNew = { x, y };
if (PtInRect(&g_rcExclude, ptNew)) {
// Clamp to nearest legal position
int leftMargin = ptNew.x - g_rcExclude.left;
int topMargin = ptNew.y - g_rcExclude.top;
int rightMargin = g_rcExclude.right - ptNew.x;
int bottomMargin = g_rcExclude.bottom - ptNew.y;
int dx, dy;
int x, y;
if (leftMargin < rightMargin) {
x = g_rcExclude.left;
dx = leftMargin;
} else {
x = g_rcExclude.right;
dx = rightMargin;
}
if (topMargin < bottomMargin) {
y = g_rcExclude.top;
dy = topMargin;
} else {
y = g_rcExclude.bottom;
dy = bottomMargin;
}
if (dx < dy) {
ptNew.x = x;
} else {
ptNew.y = y;
}
// POINT ptScreen = ptNew;
// ClientToScreen(hwnd, &ptScreen);
// SetCursorPos(ptScreen.x, ptScreen.y);
}
if (g_pt.x != ptNew.x || g_pt.y != ptNew.y) {
RECT rcItem = ItemRect(g_pt);
InvalidateRect(hwnd, &rcItem, TRUE);
g_pt = ptNew;
rcItem = ItemRect(g_pt);
InvalidateRect(hwnd, &rcItem, TRUE);
}
}
This time, we don’t try to punish you for moving the mouse into the forbidden rectangle. But the object won’t follow the mouse into a forbidden region.
Seems like this is a classic drag and drop problem. Try this: in File Explorer if you drag a selection of files or folders to an illegal spot to drop them, the cursor changes to a "not allowed" cursor (circle with a slah through it).
Thats the behavior a user expects. If they let go of the mouse button while in an illegal area... nothing happens. As it should (technically shouldn't).
Here's why implementing that behavior is so important - users rely on that behavior. Let me give a concrete example.
Outlook desktop has a maddening feature... if you grab the wrong item...
In my experience, pressing the Esc key during a drag operation will safely abort it no matter where the mouse cursor happens to be. Pressing a keyboard key while holding down the left mouse button can be difficult in some situations, though.
Your point still stands, of course. People need an easily discoverable way to abort an unintended drag operation, and Esc is hardly discoverable. Depending on the user, they may also need something more accessible than reaching out to the keyboard.
The title of the post brings memories of INT 33h AX=10h.
On a more modern note, a vendor sells laptop computers that have a camera notch at the top of the screen. The mouse/touchpad pointer cannot move to notch area, but the left and right side of the notch don’t push the pointer away; the pointer instead warps to the other side of the notch. Does Windows have APIs or DDIs for that behaviour? … I suppose EDID would be the correct place for configuring this because the notch is a property of the display hardware.
I could see a way to actually constraint the cursor, but it would be both inadvisable (see Dan Bugglin's prior comment) and very complicated:
It would involve following the cursor movement to keep track of which quadrant the cursor is in, and having a cursor clipping region following the cursor.
Such as, if the cursor is in the Right quadrant (meaning it's to the right of lines jutting at 45° from the rectangle's corners) we set a clipping region that starts at the rectangle's right border.
Not only uselessly complicated compared to the elegant solution, but it would further bloat in...
This is a good example of "if you are finding it difficult to do something, stop and consider that maybe the reason it wasn't made easy to do is because it is a bad idea to do it".
Having control of my mouse yanked away from me is annoying. In extreme cases it can feel like my computer is fighting me. My computer should be helping me to accomplish tasks, not fighting me. The first thing I do is a three-finger salute, and Windows generally cancels any clipping etc when you do this.
ClipCursor and manually repositioning the cursor should only be...