Board index » delphi » Listview starts dragging: Bug or just odd behaviour?

Listview starts dragging: Bug or just odd behaviour?

aste...@nospam.iafrica.com (Anthony Steele) wrote in
<8fbm3s$1...@bornews.borland.com>:

Quote
>unit Unit1;

>{ test program by Anthony Steele
> 10 May 2K
> This program is cut down from a real world program that
> shows a "Do you want to save changes to the current item' Yes/No/Cancel
> dialog box when the current item has been modified & the user selects a
>different item
> It seems that putting up a dialog from the Changing event somehow
> causes the list view to start dragging.

>    To exhibit the bug: Compile and run
>  Click on an item. press OK  on the messagebox. You are now in drag
>  mode.
>You shouldn't be.

>- How can I avoid this??
>- Is this a VCL bug or just a "feature" that is working against me?
>- Should I log this with Borland & the Delphi bug lisf?
>}

Anthony,

In the listview, set DragMode to dmManual.

HTH,

Chris.
---------

 

Re:Listview starts dragging: Bug or just odd behaviour?


Quote
> Anthony,

> In the listview, set DragMode to dmManual.

> HTH,

Hm, I should have expected that. The proposed fix doesn't help. DragMode is
not dmManual, because this program is cut down from a real world program
that has two listviews, and yes, you can drag between them, so I want
DragMode to be dmAutomatic. The problem is that dragging is turned on at
unwanted times.

I tried to fix by setting DragMode to dmManual and coding as follows:

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
begin
  if Change = ctState then
  begin
    ShowMessage('Changing');
    if (ListView1 <> nil) and (Item <> nil) then
      ListView1.BeginDrag(false, 5);
  end;
end;

but
a) This still has the bug
b) It doesn't drag
c) It gives access violations

so that's not hopeful.

I also tried to code around it like this, but this version also has severe
problems

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
begin
  if ListView1 = nil then
    exit;

  if csDestroying in ListView1.ComponentState then
    exit;

  if Change = ctState then
  begin
    ListView1.DragMode := dmManual;
    Application.ProcessMessages;
    ShowMessage('Changing');
    ListView1.DragMode := dmAutomatic;
  end;
end;

BTW, I am running Delphi 5.0 on Win2K

l8r
Anthony
aste...@nospam.iafrica.com

Re:Listview starts dragging: Bug or just odd behaviour?


Quote
Anthony Steele <aste...@nospam.iafrica.com> wrote in message

news:8fdr44$jh54@bornews.borland.com...

Quote
> > Anthony,

> > In the listview, set DragMode to dmManual.

> > HTH,

> Hm, I should have expected that. The proposed fix doesn't help. DragMode
is
> not dmManual, because this program is cut down from a real world program
> that has two listviews, and yes, you can drag between them, so I want
> DragMode to be dmAutomatic. The problem is that dragging is turned on at
> unwanted times.

you could try setting the AllowChange variable to False when you dont need
the change
by default is is True.

Re:Listview starts dragging: Bug or just odd behaviour?


Quote
"Raghavendra Rao" <ragh...@geocities.com> wrote in message

> you could try setting the AllowChange variable to False when you dont need
> the change by default is is True.

Right. You did run the sample code that I posted? I'm not sure how this
suggestion relates to unwanted dragging, as starting to drag is not a change
that calls the OnChanging event far as I know (OnStartDrag is signalled, and
you don't have the option to not Accept).

However this brings me to a percieved deficiency in the List view, ie that
the Changing event is too broad and it is hard to distinguish between the
different cases:

go back to the original sample, make sure that DragMode is dmAutomatic, and
set up the event handler like this:

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
begin
  if ListView1 = nil then
    exit;

  if Item = nil then
    ShowMessage('Changing nil')
  else if Item = ListView1.Selected then
    ShowMessage('Changing sel')
  else
    ShowMessage('Changing not sel');

end;

Besides the drag mode bug, the output when changing from one item to another
is as follows:
Changing sel
Changing sel
Changing not sel
Changing sel

*how can these 4 invocations be reliably distingished?* How can I deal only
with the current item being deselected (and approve or disaprove of the
change as a whole), without having to swim through the rest of this stuff?

The intension of the original code was as follows (in pseudocode)

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
begin

  if IsObjectDirty(Item.Data) then
    Case YesNoCancelDialog('Do you want to save chnages?') of
      Yes:  Save(Item.Data);
      No ;
      Cancel: AllowChange := False;
    end;

end;

There is no need to go through this procedure 4 times. Once will do, if you
manage to get the right one. And I suspect that the dragging bug may be
caused by doing things at the wrong time.
But someone at either Microsoft or Borland decided that it would be fun to
make thier users to play blind-mans-buff.

The docs say Change: TItemChange can be "ctState: A change to the list item'
s Cut, Focused, or Selected property" so how do I distinguish between these
three cases?

l8r
Anthony

Re:Listview starts dragging: Bug or just odd behaviour?


On Wed, 10 May 2000 15:06:35 +0200, "Anthony Steele"

Quote
<aste...@nospam.iafrica.com> wrote:
>unit Unit1;

>{ test program by Anthony Steele
> 10 May 2K
> This program is cut down from a real world program that
> shows a "Do you want to save changes to the current item' Yes/No/Cancel
> dialog box when the current item has been modified & the user selects a
>different item
> It seems that putting up a dialog from the Changing event somehow
> causes the list view to start dragging.

>    To exhibit the bug: Compile and run
>  Click on an item. press OK  on the messagebox. You are now in drag mode.
>You shouldn't be.

Here's what I think is the sequence of events:

  WM_LBUTTONDOWN VCL Listview
    Set FClicked:= false;
    WM_LBUTTONDOWN comctl32.dll
      Send item changing notification
        Pop up dialog
          (somewhere in here, the WM_LBUTTONUP message is processed; since the
           listview does not get this message, it does not dispatch an NM_CLICK
           notification.  Had the VCL listview received the NM_CLICK, it would
           have set FClicked to true}
  WM_LBUTTONDOWN VCL Listview
    After calling the inherited method, the listview assumes that, if FClicked
    is false, a drag has started, so (if DragMode is dmAutomatic) it does
    BeginDrag

The root of the problem is that the listview code in comctl32.dll handles a
WM_LBUTTONDOWN by entering its own modal loop until 1) the button is released
(at which point it sends an NM_CLICKED notification) or 2) the mouse crosses a
drag threshhold (at which point it sends an LVN_BEGINDRAG notification).  The
VCL code in TCustomListView.WMLButtonDown is trying to determine which one of
these two events caused the modal loop to exit.  Unfortunately, popping up the
dialog causes a click exit without an NM_CLICK.

It looks to me like it would be better to do what the treeview does: use an
FDragged flag which is set to true when the listview gets an LVN_BEGINDRAG.  So
you might consider this a bug in the VCL.  On the other hand, I believe you're
violating a lot of assumptions by popping up that dialog in the itemchanging
event.  Anyway, I am not sure this will work, but you might try sending the
listview an NM_CLICK notification (Message = CN_CLICK, NMHdr.code = NM_CLICK)
when you pop up your dialog.  That should convince the TListView that a click
has occurred (so it will not enter drag mode).

Greg Chapman

Re:Listview starts dragging: Bug or just odd behaviour?


aste...@nospam.iafrica.com (Anthony Steele) wrote in
<8fdr44$j...@bornews.borland.com>:

Quote
>> Anthony,

>> In the listview, set DragMode to dmManual.

>> HTH,

>Hm, I should have expected that. The proposed fix doesn't help. DragMode
>is not dmManual, because this program is cut down from a real world
>program that has two listviews, and yes, you can drag between them, so I
>want DragMode to be dmAutomatic. The problem is that dragging is turned
>on at unwanted times.

>I tried to fix by setting DragMode to dmManual and coding as follows:

>procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
>  Change: TItemChange; var AllowChange: Boolean);
>begin
>  if Change = ctState then
>  begin
>    ShowMessage('Changing');
>    if (ListView1 <> nil) and (Item <> nil) then
>      ListView1.BeginDrag(false, 5);
>  end;
>end;

Anthony,

I posted a zipped-up example app in borland.public.attachments which
demonstrates using the dmManual DragMode to drag items between two
listviews.  

If you can't get that group, e-mail me (ctimm...@lgrs.com) and I'll e-mail
you the zip file.

HTH,

Chris.
---------

Re:Listview starts dragging: Bug or just odd behaviour?


Quote
> It looks to me like it would be better to do what the treeview does: use
an
> FDragged flag which is set to true when the listview gets an
LVN_BEGINDRAG.  So
> you might consider this a bug in the VCL.  On the other hand, I believe
you're
> violating a lot of assumptions by popping up that dialog in the
itemchanging
> event.

How else to do a dialog for "The item has changed - do you wan to save it:
Yes/No Cancel", with cancel taking you back to the old item? At present I
have disabled all that and am always saving changes.

Strange that you should mention Tree view, as I first did this on a tree
view - I though that reimplmenting this on list view would be simpler, as a
List view is not as complex as a tree view. But no, life is not that easy.

Quote
> Anyway, I am not sure this will work, but you might try sending the
> listview an NM_CLICK notification (Message = CN_CLICK, NMHdr.code =
NM_CLICK)
> when you pop up your dialog.  That should convince the TListView that a
click
> has occurred (so it will not enter drag mode).

I am in awe of your WIndows API knowledge. After some bashing through MSDN
to work out what you meant, I have coded it (correctly I hope). The
implementation below runs, and does not exhibit the dragging bug, but it
also never drags

unit Unit1;

{ test program by Anthony Steele
 12 May 2K comment as before }

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ImgList, ComCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    ImageList1: TImageList;
    procedure ListView1Changing(Sender: TObject; Item: TListItem;
      Change: TItemChange; var AllowChange: Boolean);
  private
    nmHDR: TNMHdr;
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

uses CommCtrl;

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
begin

  if ListView1 = nil then
    exit;
  if csLoading in ListView1.ComponentState then
    exit;
  if csDestroying in ListView1.ComponentState then
    exit;

  if Change = ctState then
  begin
    nmHDR.hwndFrom := ListView1.Handle;
    nmHDR.code := NM_CLICK;
    ListView1.Perform(WM_NOTIFY, 0, integer(@nmHDR));
    ShowMessage('Changing');
  end;

end;

end.

---dfm as before---

Re:Listview starts dragging: Bug or just odd behaviour?


On Fri, 12 May 2000 12:44:33 +0200, "Anthony Steele"

Quote
<aste...@nospam.iafrica.com> wrote:

>procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
>  Change: TItemChange; var AllowChange: Boolean);
>begin

>  if ListView1 = nil then
>    exit;
>  if csLoading in ListView1.ComponentState then
>    exit;
>  if csDestroying in ListView1.ComponentState then
>    exit;

>  if Change = ctState then
>  begin
>    nmHDR.hwndFrom := ListView1.Handle;
>    nmHDR.code := NM_CLICK;
>    ListView1.Perform(WM_NOTIFY, 0, integer(@nmHDR));
>    ShowMessage('Changing');
>  end;

>end;

I'm rather surprised that the above worked.  Here's what I was thinking of:

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
  Change: TItemChange; var AllowChange: Boolean);
var nmHdr: TNMHdr;  {no reason this can't be local, since it's not
                     being used in a PostMessage}
    ListView: TListView;
begin
  ListView:= Sender as TListView;
  if csLoading in ListView.ComponentState then
    exit;
  if csDestroying in ListView.ComponentState then
    exit;

  if Change = ctState then
  begin
    nmHDR.hwndFrom := ListView.Handle;
    nmHDR.idFrom := -1;
      {probably should initialize the idFrom field; if I remember correctly,
       Microsoft often uses -1 as the ID of controls which don't really have an
       ID.  At the moment, the VCL doesn't pay any attention to the idFrom
       field, but you never know.}
    nmHDR.code := NM_CLICK;
    ListView.Perform(CN_NOTIFY, 0, integer(@nmHDR));
                    {^^^^^^^^^}
    ShowMessage('Changing');
  end;

end;

Normally, WM_NOTIFY messages are sent to the parent of the control which is
generating them.  The VCL code in controls.pas, when it handles WM_NOTIFY on
behalf of the parent control, maps these messages to CN_NOTIFY (a VCL defined
message) and sends them on to the VCL code for the child control which generated
the notification.  If you have the source, you'll see in comctrls.pas that
TCustomListView.CNNotify sets FClicked to true when it gets an NM_CLICKED
notification.

Ideally, the above code would also only execute when the state change involves a
selection change (or perhaps a focus change).  Unfortunately, the VCL control
makes it somewhat difficult to break down exectly what part of an item's state
has changed.  If you're comfortable writing a TListView descendant, you can add
a CNNotify message handler which has a case for the LVN_ITEMChanging
notification.  If you check out this notification in the windows API help file,
you'll see that you can determine what part of the state has changed.

As to your larger question of how to prompt for saving before focus moves from
an item, unfortunately, I don't have any suggestions concerning the listview.
However, I wonder if you can accomplish what you want with a TStringGrid (or
TDrawGrid) with the save prompt used in an OnSelectCell handler.  I just tried a
quick test, and it looked like that should work, provided, of course, that your
code will work with one of the grids.

Greg Chapman

Re:Listview starts dragging: Bug or just odd behaviour?


"Greg Chapman" <g...@well.com> wrote

Quote
>  If you're comfortable writing a TListView descendant, you can add
> a CNNotify message handler which has a case for the LVN_ITEMChanging
> notification.

Good grief, it almost works! The only remaining problem is that when I say
"no, do not allow the change" I get that message 3 times over. Any clues at
to what I'm doing wrong here?

unit ListViewEx;

{ AFS 14 May 2K
  a quick & diry version to test item select events in a list view
  code after suggestions & API knowlede from Greg Chapman

Quote
}

interface

uses ComCtrls, CommCtrl, Controls, Messages, Windows;

type
  TListViewItemChangeEvent = function(const feOldState, feNewState:
TItemStates): boolean of object;

  { should derive from TCustomListView }
  TListViewEx = class(TListView)
    private
      fcOnAllowChanging: TListViewItemChangeEvent;

      procedure CNNotify(var Message: TWMNotify);  message CN_NOTIFY;

      procedure ItemChanging(var ListViewChange: TNMListView; var Result:
Longint);
      function AllowChange(const feOldState, feNewState: TItemStates):
Boolean;

    protected
    public
      procedure FakeClick;

    published
      property OnAllowChanging: TListViewItemChangeEvent read
fcOnAllowChanging write fcOnAllowChanging;

  end;

implementation

function HasFlag(a, b: integer): Boolean;
begin
  Result := (a and b) <> 0;
end;

function BooleanToInteger(pb: Boolean): longint;
begin
  if pb then
    Result := 1
  else
    Result := 0;
end;

{ from ComCtrls}
function ConvertStates(State: Integer): TItemStates;
begin
  Result := [];
  if HasFlag(State, LVIS_ACTIVATING) then
    Include(Result, isActivating);
  if HasFlag(State, LVIS_CUT) then
    Include(Result, isCut);
  if HasFlag(State, LVIS_DROPHILITED) then
    Include(Result, isDropHilited);
  if HasFlag(State, LVIS_FOCUSED) then
    Include(Result, isFocused);
  if HasFlag(State, LVIS_SELECTED) then
    Include(Result, isSelected);
end;

procedure TListViewEx.CNNotify(var Message: TWMNotify);
begin
  inherited;

  with Message do
  begin
    case Message.NMHdr^.code of
      LVN_ITEMChanging:
        ItemChanging(PNMListView(Message.NMHdr)^,  Message.Result);
    end;
  end;
end;

procedure TlistViewEx.ItemChanging(var ListViewChange: TNMListView; var
Result: Longint);
var
  leOldState, leNewState: TItemStates;
begin
  leOldState := ConvertStates(ListViewChange.uOldState);
  leNewState := ConvertStates(ListViewChange.uNewState);

  { MSDN says "Return FALSE to allow the change" }
  Result := BooleanToInteger(not AllowChange(leOldState, leNewState));
end;

function TlistViewEx.AllowChange(const feOldState, feNewState: TItemStates):
Boolean;
begin
  Result := True;

  { call the event handler if it exists }
  if Assigned (fcOnAllowChanging) then
    Result := fcOnAllowChanging(feOldState, feNewState);

end;

procedure TListViewEx.FakeClick;
var
  nmHDR: TNMHDR;
begin
  nmhdr.idFrom := 0;
  nmHDR.hwndFrom := Handle;
  nmHDR.Code := NM_CLICK;

  Perform(CN_NOTIFY, 0, integer(@nmHDR));
end;

end.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ImgList, ComCtrls,

  { local} ListViewEx;

type
  TForm1 = class(TForm)
    ImageList1: TImageList;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    lv1: TListViewEx;

    function CanChange(const feOldState, feNewState: TItemStates): boolean;

  public
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

uses CommCtrl;

function Query: Boolean;
begin
  result := (MessageDlg('Allow the change', mtConfirmation, [mbYes, mbNo],
0) = mrYes);
end;

function TForm1.CanChange(const feOldState, feNewState: TItemStates):
boolean;
begin
  { by default }
  Result := True;

  { allow item to be deselected ? }
  if (isSelected in feOldState) and (not (isSelected in feNewState)) then
  begin
    Result := Query;
    if Result then
      lv1.FakeClick; // stop the dragging before it begins

  end;

  { as a corollary of the above, don't allow an item to become selected
    if another item is still selected (eg if the deselect was denied) }
  if (not (isSelected in feOldState)) and (isSelected in feNewState)
   and (lv1.Selected <> nil) then
  begin
    Result := False;
  end;

end;

procedure TForm1.FormCreate(Sender: TObject);
var
  lcCol: TlistColumn;
  lcItem: TlistItem;
begin
  { early test - create manually rather than put it on the toolbar }
 lv1 := TlistViewEx.Create(self);
 lv1.Left := 10;
 lv1.Top := 10;
 lv1.ViewStyle := vsIcon;
 lv1.ReadOnly := True;
 lv1.OnAllowChanging := CanChange;

 lcCol := lv1.Columns.Add;
 lcCol.Caption := Name;
 lcCol.Width := 100;

 lv1.DragMode := dmAutomatic;
 lv1.Parent := Self;
 lv1.Visible := True;

 lcItem := lv1.Items.Add;
 lcItem.Caption := 'Fred';

 lcItem := lv1.Items.Add;
 lcItem.Caption := 'Jim';

 lcItem := lv1.Items.Add;
 lcItem.Caption := 'Mary';
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(lv1);
end;

end.

Re:Listview starts dragging: Bug or just odd behaviour?


As someone else already said, it is not a good idea to put up a dialog while
in the OnChange event. Showing a message/dialog causes the OnChange event
to trigger all over again. In the OnEditing event, set AllowEdit to false and
post a custom message to your app to handle editing, e.g.
    UM_ONLVEDIT = WM_USER + 1;

procedure form1.listviewonediting(Sender: TObject; Item: TListItem; var
AllowEdit: Boolean);
begin
     allowedit := false;
     postmessage(handle,UM_ONLVEDIT,0,0)
end;

procedure UMOnLVEdit(var msg : TMessage);message UM_ONLVEDIT;
var
   s : string;
begin
       s := listview.selected.caption;
        if InputQuery(Caption,'Enter new text', s) then
          if AnsiCompareText(s, listview.selected.caption) <> 0 then
               <if data is acceptable to you> then
                  listview.selected.caption := s;
end;

As for what is happening in OnChange event, it's something like this
     if item = nil then exit;
     if ctstate in [change] then begin
          if (item.selected) but not focused then
             it isn't fully selected
          but if (item.selected) and (item.focused) then
              it is fully selected

If you are using D5, the listview has an event called OnSelectItem which
eliminates the necessity for stressing yourself with all these state changes
since OnSelectItem isn't called until all these state changes have been
resolved and the item is both selected and focused.

Anthony Steele said something like

Quote

> "Greg Chapman" <g...@well.com> wrote

> >  If you're comfortable writing a TListView descendant, you can add
> > a CNNotify message handler which has a case for the LVN_ITEMChanging
> > notification.

> Good grief, it almost works! The only remaining problem is that when I say
> "no, do not allow the change" I get that message 3 times over. Any clues at
> to what I'm doing wrong here?

> unit ListViewEx;

Re:Listview starts dragging: Bug or just odd behaviour?


Quote
"lj" <linda...@bellatlantic.net> wrote in message

news:MPG.1388adfcd983263f9899c8@forums.inprise.com...

Quote

> As someone else already said, it is not a good idea to put up a dialog
while
> in the OnChange event.

Yeah, I'm starting to get that impression, or at least the impression that
the designers of this control did not cater for that idea.

Quote
> Showing a message/dialog causes the OnChange event
> to trigger all over again. In the OnEditing event, set AllowEdit to false
and
> post a custom message to your app to handle editing, e.g.
>     UM_ONLVEDIT = WM_USER + 1;

This assumes that I am doing editing in place on the caption. I am doing
nothing of the sort so the OnEditing event is irrelevant to me.

The item in the list has a pointer to an object that is displayed in detail
in an edit frame below the list view. When changing from one item to another
in the list view, the item, the edit frame is queried to see if it has
changes, if so, then the "Change selection and save object changes/Change
selection and don't save object changes/Don't change selection" dialog box
is brought up. The last sample that I posted is a good model of the desired
behavior.

Quote
> As for what is happening in OnChange event, it's something like this
>      if item = nil then exit;
>      if ctstate in [change] then begin
>           if (item.selected) but not focused then
>              it isn't fully selected
>           but if (item.selected) and (item.focused) then
>               it is fully selected

> If you are using D5, the listview has an event called OnSelectItem

Yup, I am using this already to notify the edit frame. I'll do some more
poking about tomorrow to see if I can get a flag set up to prevent the
messagebox repeating.

l8r
Anthony

Other Threads