TListBox Drag and Drop problems with MultiSelect enabled - delphi

I have a TListBox with multiselect and ExtendedSelect both set to true. I need to be able to drag multiple items in the list box to re-arrange them. My problem is what happens when the user clicks on an item that is already selected without holding down the CTRL or SHIFT key.
Case 1: DragMode is set to dmManual
The selection is cleared before the mouse down. This will not allow multiple items to be dragged.
Case 2: DragMode is set to dmAutomatic
The MouseDown event never fires. The selection is not cleared so dragging is OK, but the user cannot clear the selection by clicking on one of the selected items. This really causes a problem if all the items are selected or the next item the user wants to select was part of the current selection.
Note that this problem only happens if you assign something to the DragObject in the OnStartDrag procedure. I think the problem would go away if OnStartDrag would only start after the user moves the mouse. I have Mouse.DragImmediate := false set but I still get the StartDrag fired as soon as I click on an item in the list box.
I am using Delphi 7 for this project but I see the same behavior in Delphi 2007.

I have played with this for a while. And observe the same effects.
I would use Case2 and add a (Select All/Deselect All) button to the list. It even adds extra functionality and solves the most annoying part of the problem.

Use Case 2 and when the TListBox.OnMouseUp event fires check to see if multiple items are selected and were dragged. If multiple items are selected, but weren't dragged, then deselect all items apart from the clicked item.
I would use this method because Windows Explorer works this way.

Bit of a kludge but this works. DragMode on the ListBox is set to dmAutomatic.
procedure TForm1.ListBox1DragDrop(Sender, Source: TObject; X, Y: Integer);
var
iDropIdx, i: Integer;
pDropPoint: TPoint;
slSelected: TStrings;
begin
{Which item is being dropped onto?}
pDropPoint := Point(X, Y);
iDropIdx := ListBox1.ItemAtPos(pDropPoint, False);
slSelected := TStringList.Create;
try
{Copy the selected items to another string list}
for i := 0 to Pred(ListBox1.Items.Count) do
begin
if (ListBox1.Selected[i]) then
slSelected.Append(ListBox1.Items[i]);
end;
{Find the selected items in the listbox and swap them with the drop target}
for i := 0 to Pred(slSelected.Count) do
begin
ListBox1.Items.Exchange(ListBox1.Items.IndexOf(slSelected[i]), iDropIdx);
inc(iDropIdx);
end;
finally
slSelected.Free;
end;
end;

I'm not sure why this makes a difference but if I change the DragObject to be a TDragControlObjectEx (instead of a TDragObjectEx) I get the behavior I am looking for. Drag mode is set to Automatic.
I tried to look and see what this was affecting but I could not figure it out.

Related

How do I assure that mouse pointer appears when mouse moves over form after commanding the drop down for a combobox to show?

I have a problem where:
I have a form with just a combobox.
The combobox has focus and mouse is not hovering over the form when item 3 happens.
I trigger the combobox's drop-down list to show on a key-press event.
When the drop-down list is visible and then I move my mouse pointer over the form, the pointer is either invisible, shows that it is busy, or shows the resizing icon but does not turn back to a normal pointer when over the form.
Is there something that can be done to assure that, when the drop-down of the combobox shows, that the mouse pointer is visible when I move the pointer over the form?
I have tried:
Applicaiton.ProcessMessages after showing the drop-down.
Changing focus to the form the combobox is on after showing the drop-down.
Adding Key := #0; after calling the drop-down to show.
procedure TForm1.ComboBox1KeyPress(Sender: TObject; var Key: Char);
begin
SendMessage(ComboBox1.Handle, CB_SHOWDROPDOWN, Integer(True), 0);
Key := #0;
end;
Tried using a timer to trigger the drop-down within the key-press event.
Tried using "SetCursor" after commanding the drop-down to appear.
Tried using ".DroppedDown", but did not see any difference in result from that of "SendMessage".
I would hope to be able to show the mouse pointer after the drop-down is displayed, but it is hidden instead. Thanks for any suggestions.
(NOTE: This problem I have run into is not exlusive to Delphi. I was able to duplicate the issue using Visual C# 2017. Either way, if there is a way to correct this, it would be good to know).
As already commented to the question, the issue is not Delphi related. You can observe the same behavior in dialog boxes which contains a similar combo that the OS presents. One example is the one on the "run" dialog.
Involving a single environment, re-setting the cursor in an OnDropDown event handler fixes the problem.
procedure TForm1.ComboBox1DropDown(Sender: TObject);
begin
winapi.windows.SetCursor(Screen.Cursors[Cursor]);
end;
Originally I tested the above because no one calls SetCursor after the drop down. Though it seems that no one calls it before either. So I have no idea about the cause or why the above fix works.

Selecting Listitems with Drag not working

On the MouseMove event I need to select items in the Listview if my mouse click is dragging over them. My code however is not working. When I click and drag only the first item I click gets selected.
In the MouseMove event:
//If left mouse button is depressed
if(GetAsyncKeyState(VK_LBUTTON) = 1) then
begin
LListItem := NestingResultsListView.GetItemAt(x,y);
//If the item is not selected, select it.
if not LListItem.Selected then
begin
LListItem.Selected := true;
end;
end;
TListView has a MultiSelect property, check that in the Object Inspector and then you can select multiple items with mouse dragging, you don't need to code it yourself.
First of all, GetAsyncKeyState returns the 'down' state in most significant bit of the word, so you should write something like GetAsyncKeyState(VK_BUTTON) and $8000 <> 0.
Second, using GetAsyncKeyState for mouse buttons is not a very good thing since it checks for physical buttons (and if a user is left-handed and remaps the buttons he will be confused as your code expects left mouse button to be pressed). A better way would be to remember pressed mouse buttons in OnMouseDown event and update/reset them in OnMouseUp event.

Making sure at least 1 item in a TPopup menu is always selected

I have TPopup menu with 3 items setup with the following properties.
AutoCheck = True
RadioItem = True
and for the first menu item I set the Checked property to True as well so the menu has at least 1 item selected with it is initially displayed. This is all good and well, but if I click on the item that is currently selected, the check (dot) goes away and I want to make it so that at least 1 item in the popup is selected at all times. How do I go about doing that?
The items in the group should also have the same non-zero GroupIndex. This, together with RadioItem should make the items behave like a radio group.
However, it seems that AutoCheck doesn't respect GroupIndex and RadioItem properties, since it will clear the check when you click on a currently checked item.
(Arguably, this could be considered a bug in the VCL.)
Here's a workaround:
Set AutoCheck to False, assign all items the same non-zero GroupIndex and a common OnClick handler to check the clicked item like this:
procedure TForm1.ItemClick(Sender: TObject);
begin
(Sender as TMenuItem).Checked := True;
end;
Or use Actions, with them this seems to work as expected.
You can set following OnClick handler to all your radio group Menu Items:
procedure TForm1.MenuItemClickHandler(Sender: TObject);
begin
if Sender is TMenuItem then
begin
if not (Sender as TMenuItem).Checked then (Sender as TMenuItem).Checked:=True;
end;
end;

How to add a drop down menu to an Action Item at Runtime

I'm using the following method to create a an ActionClient with an Action at run time.
procedure TMainForm.AddToProjectHistory(Path: string);
var
NewOption: TAction;
ActionClient: TActionClientItem;
begin
NewOption := TAction.Create(self);
NewOption.ActionList := ActionManager1;
NewOption.Caption := Path;
NewOption.OnExecute := ProjectHistoryExecute;
ActionClient := TActionClientItem(aToolBarFile.ActionClient.Items[0].Items.Add);
ActionClient.Action := NewOption;
ActionClient.Caption := Path;
end;
This works fine if there is already an item in the list, but doesn't work at all if there isn't
e.g. if I add an Item at design time then I can add more items at runtime
But if I don't add anything at design time, theres no Drop Down to display the list of items, no drop down appears after adding items.
This doesn't have to be done with Actions but the rest of the menu system uses actions and I don't think I can add standard MenuItems to the action drop down.
Delphi 2005
The VCL automatically creates button controls of a type that depends on whether the item has child elements. By default (and depending on the ActionManager's style setting), for an ActionClientItem which has child items, a TXPStyleDropDownBtn button is created, and for a childless ActionClientItem, a TXPStyleButton is created.
So when the first child item is added during run time, the button is of the wrong type. Changing the type of that button would require destruction of the current button and a complete and manual instantiation of the new button. This should be possible, but have not tried, because:
The really most easy solution is to fool the VCL by adding a child item at design time, and to delete that item on form creation:
procedure TForm1.FormCreate(Sender: TObject);
begin
aToolBarFile.ActionClient.Items[0].Items[0].Free;
end;
if you create an actionclient of type "context", it won't have to change button type. The menu will drop down when you right click on the button instead.

Delphi: TabStop problems of TRadioButton

When TRadioButton has TabStop=True, it's acting very strange.
If you will try to switch focus between many radio buttons on a form using Tab key, you would do it only 1 time per app session. The tabulation is one-way, never returning back to the first radio button. Also when the focus is moving across radio buttons, they becoming "checked" automatically.
Can this behavior be fixed without creating my own component?
I want standard radio buttons to
switch focus cyclically
prevent radio button from checking when the focus comes into it (I want my users to check them using Space key)
I understand that you're working with existing code, which is a real world constraint that's too often dismissed in these forums.
Sounds to me like checkboxes would suit you better. You can enforce the exclusivity normally expected of RadioButtons in the OnChecked event. That should solve your tabbing/focus and selection/deselection issues.
Checkboxes won't be checked automatically upon receiving focus, and your users can check/uncheck them with the space key.
You can put code in the OnEnter event to prevent the checkbox from selecting.
You'll need to store the previously selected RadioButton somehow though.
var
SelectedRadioButton: TRadioButton;
//event shared by all radiobuttons
procedure TForm1.RadioButton1Enter(Sender: TObject);
begin
if Sender <> SelectedRadioButton then begin
SelectedRadioButton.Checked:= true;
end;
end;
procedure TFrameOrder.RadioButton1Click(Sender: TObject);
begin
SelectedRadioButton:= (Sender as TRadioButton);
end;
procedure TFrameOrder.RadioButton1KeyPress(Sender: TObject; var Key: Char);
var
MyRadioButton: TRadioButton;
begin
MyRadioButton:= (Sender as TRadioButton);
if Key in [#32,#13] then begin
MyRadioButton.Checked:= true;
RadioButton1Click(MyRadioButton);
end; {if}
end;
It probably clearer to create a new TMyRadioButton component though because this will clutter up your regular code.
I have found an interesting article of Craig Stuntz about this problem. As I can see, I'll need to create my own control to solve it.
By default only one RadioButon has property TabStop = True;
All Radiobuttons are treated as one controll.
When radiobutton has focus you can switch beetween radiobutons using arrow up and down.
Now when user choose one option they can press tab to switch to another controll (without changing radio options).

Resources