>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.
---------
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
ast...@nospam.iafrica.com
you could try setting the AllowChange variable to False when you dont need
the change
by default is is True.
>
> 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
>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
>> 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 (ctim...@lgrs.com) and I'll e-mail
you the zip file.
HTH,
Chris.
---------
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.
> 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---
>
>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
> 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
}
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.
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
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.
> 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.
> 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
Green was selected. Now click on blue:
ODS: ONCHANGE: state selected green
ODS: ONSELECTITEM: green selected
ODS: ONCHANGE: state selected & focused blue
ODS: ONSELECTITEM: blue selected
ODS: MouseDown event with blue
I no longer see any reason to prefer OnSelectItem over OnChange.
> 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.
Is it possible to let user just complete the dragging operation uninterrupted
and then put up confirmation dialog box after the drag has ended? Explorer
does this when you try to drag a file to another
folder that has a file with the same name. After you are done dragging, you
are asked if you want to replace the file or cancel.
I hope your custom listview with the drag op fixes works for you, but I would
not trust a dmAutomatic drag mode, especially when combined with dialog
boxes, unless you let the drag proceed uninterrupted and have no other use
for mousedown events.
BTW, using dmManual mode, the following code works for me (warning-code not
fully tested):
const
um_confirm = wm_user + 1;
private
FConfirmed, FBeginDrag : Boolean;
FEntering : Boolean;
procedure umconfirm(var msg : TMessage); message um_confirm;
public
{ Public declarations }
end;
procedure TForm1.ListView1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FConfirmed then
if (Button = mbLeft) and (listview1.GetItemAt(x,y) <> nil) then
listview1.BeginDrag(False);
end;
procedure TForm1.ListView2DragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
begin
if source is TListView then
accept := (TListView(source).handle = listview1.handle);
end;
procedure TForm1.ListView2DragDrop(Sender, Source: TObject; X, Y: Integer);
var
item : TListItem;
begin
item := listview2.GetItemAt(x,y);
if item = nil then
item := listview2.Items.Add
else
item := listview2.Items.Insert(item.index);
item.caption := listview1.selected.caption;
end;
procedure TForm1.ListView1Change(Sender: TObject; Item: TListItem;
Change: TItemChange);
begin
if item = nil then exit;
if item.focused and item.selected then begin
FConfirmed := False;
postmessage(handle,um_confirm,0,0);
end;
end;
procedure TForm1.ListView1Enter(Sender: TObject);
begin
FEntering := True;
if (listview1.selected = nil) and (listview1.items.count > 0) then begin
listview1.items[0].selected := true;
listview1.items[0].focused := true;
end;
end;
procedure TForm1.umconfirm(var msg: TMessage);
begin
if not FEntering then showmessage('save changes?');
FEntering := False;
FConfirmed := True;
if FBeginDrag and (listview1.Selected <> nil) then begin
FBeginDrag := False;
BeginDrag(False);
end;
FBeginDrag := false;
end;
Good luck
Try your tests again using the keyboard arrow keys for navigation.
Mike Orriss (TeamB)
(Unless stated otherwise, my replies relate to Delphi 4.03/5.00)
(Unsolicited e-mail replies will most likely be ignored)
Thanks for the tip, but I already did that some time ago when coming to grips
with virtual listviews. While it is always a good idea to re-visit these
things, dragging is the issue here, i.e. mousedowns not keydowns.