Mega Code Archive

 
Categories / Delphi / Examples
 

Fixing the List View Drag and Drop Bug

Title: Fixing the List View Drag and Drop Bug Question: The drag and drop implementation in the TListView component has a very annoying bug. If you are dragging an item too fast, the drag cursor will remain on the screen sometimes, leaving a noticable garbage on the form. In this article, I will explain the nature of this bug and show you two different ways to fix it. Answer: A long time ago, when I first tried to do drag and drop with C++Builder 1, I noticed that there was a small but very annoying problem with the TListView class. When I was dragging items from a list view control, the cursor sometimes left a small garbage on the screen. I reported this bug to Inprise, but they didn't fix it, not even in Delphi/BCB 4. Finally, almost 2 years after recognizing this bug, I managed to fix it. Before I cover how to fix this bug, I would like to show you how to reproduce it and explain the exact reason why it happens. It's very easy to create a simple test application and experiment the bug yourself. Start a new application and drop two TListView components on the form. Align them one above the other, as shown in Figure 1. Set the DragMode property for the lower list view to dmAutomatic, then set ViewStyle to vsReport and add a new column and a few items to the control. Figure 1 - A Sample Program to Reproduce the Bug Build the application and try to drag the items from the lower list view towards the upper one. If you are doing it fast enough, the drag cursor will remain on the screen, leaving a noticable garbage on your form. See Figure 2 for an example. I just did three drag and drops between the two list views, and the screen is full of garbage. Figure 2 - A Screen Shot of the Effect of the Bug To understand the reason of this bug, you need to have a look at the source code for the VCL. In the comctrls.pas file, search for the TCustomListView.DoStartDrag method. I will not show the full code due to copyright reasons, but here is the relevant part of the procedure: procedure TCustomListView.DoStartDrag(var DragObject: TDragObject); begin {...} GetCursorPos(P); P := ScreenToClient(P); {...} with P do DragItem := GetItemAt(X, Y); {...} ImageHandle := ListView_CreateDragImage(Handle, DragItem.Index, P1); {...} end; As you see, the VCL uses the GetCursorPos API function to get the current position of the cursor. Using these coordinates, the function finds out the currently selected list item and displays the drag image. The problem is that Windows is a multitasking operating system, and the GetCursorPos function is asynchronous, which means it always reports the very latest position of the cursor -- which might change too fast. The drag and drop operation is usually initiated by a click on the left mouse button. Consider the following. You click on the second item in the list view, because you want to drag it. Windows sends a WM_LBUTTONDOWN message to the application, with the current cursor coordinates. In the meantime, you move the cursor to a different location. Windows interrupts the running of your application to update the position of the cursor. VCL still didn't finish processing your events. Finally, when the DoStartDrag method starts to be executed, the cursor is at a completely different position. VCL gets the current position from Windows, and it believes that that's the start position for the drag and drop. From this moment, the behavior of the VCL is unpredictable, it is working with the wrong coordinates, it is working with the wrong drag image. From this point, anything might happen. The thing is that nothing else happens but the drag cursor remains on the screen. Now how to fix this bug? The best would be to remember the coordinates of the last mouse click before the drag and drop is initiated, and use those values instead of getting the position with GetCursorPos. It is not recommended, however, to modify and recompile the VCL. You can not even do that with C++Builder. So I have found out an easy solution that doesn't require you to edit the comctrls.pas file. You just have to subclass the TListView component and add a few new lines to it. The source code should look something like this: type TListViewFix = class(TListView) private FDragPoint: TPoint; procedure WMLButtonDown(var Message: TWMMouse); message WM_LBUTTONDOWN; protected procedure DoStartDrag(var DragObject: TDragObject); override; end; procedure TListViewFix.WMLButtonDown(var Message: TWMMouse); begin FDragPoint := ClientToScreen(Point(Message.XPos, Message.YPos)); inherited; end; procedure TListViewFix.DoStartDrag(var DragObject: TDragObject); begin if ItemFocused nil then Windows.SetCursorPos(FDragPoint.x, FDragPoint.y); inherited; end; The WMLButtonDown method traps the WM_LBUTTONDOWN message and memorizes the latest position of the cursor. Note that it does not use the GetCursorPos function, which might report the wrong position. The WM_LBUTTONDOWN message contains the exact coordinates of the cursor at the time of the click. Windows registers and sends those coordinates with the message, so there's no need to get it again. We have to override the DoStartDrag procedure and take action right before calling the inherited method. It is possible that the cursor has been moved by this time, but we can easily restore the position by calling the SetCursorPos API function. This sets the cursor back to the place where it was when the user initiated the drag and drop. After that, we immediately call the original DoStartDrag method, so it can start the drag and drop operation right away. It is true that still there might be a small delay between the SetCursorPos and GetCursorPos functions, but that is just a few instructions. Practically, that time is so short that you have no chance to move the cursor, because the computer can perform a few operations faster than you can move the mouse. This fixes the drag and drop bug and efficiently eliminates the garbage on screen. If you already have an existing application and don't want to create a new component, you can use the same idea to fix the drag and drop garbage bug right in the unit of your form. Add an OnMouseDown and an OnStartDrag event to your TListView with the following code: procedure TForm1.ListView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer) begin FDragPoint := ListView1.ClientToScreen(Point(X, Y)); end; procedure TForm1.ListView1StartDrag(Sender: TObject; var DragObject: TDragObject) begin if ListView1.ItemFocused nil then Windows.SetCursorPos(FDragPoint.x, FDragPoint.y); end; The code is almost the same as in the component version, except the FDragPoint and the event handlers are members of your form, not the list view descendant class. If you're using C++Builder, the code should look like this: void __fastcall TForm1::ListView1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { FDragPoint = ListView1-ClientToScreen(Point(X, Y)); } void __fastcall TForm1::ListView1StartDrag(TObject *Sender, TDragObject *&DragObject) { if(ListView1-ItemFocused) SetCursorPos(FDragPoint.x, FDragPoint.y); } DragPoint should be declared in the include file as follows: private: TPoint DragPoint; TTreeView suffers from the same bug, but it can be fixed the same way as in TListView.