Title: A Gardener's Guide To The Tree View: Part III
Question: In Part III of the TreeView tutorial series, we will review the code necessary to achieve drag and drop with a tree view. As always, the source code is included in the component link at the top of this article.
Answer:
First off, I'd like to say that I am seriously enjoying these tutorials. From a personal standpoint, they're helping to reinforce concepts that I've taken for granted, such as pointers and typecasting. I hope that these articles are of use to you wonderful developers out there, who are helping to strengthen the backbone of the Information Age. If you have learned anything from these tutorials, then I consider it a worthwhile effort.
If you have any questions or comments, please feel free to post a question to the forums, or to e-mail me personally at ghannodahn@icitadel.com.
Without further adieu, I will provide the anxious, impatient and/or paranoid users with the implementation source code to achieve drag and drop functionality with the TreeView component.
procedure TfrmTreeViewDemo3.tvwDemoMouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
// The tvwDemoMouseDown even fires when a mouse button is pressed
// while the cursor is sitting inside the TreeView.
var
HitNode: TTreeNode;
begin
// First, we're going to ensure that the left mouse button was
// pressed. If any other button was pressed, then we'll ignore
// this event.
if ( Button = mbLeft ) then
begin
// We'll set the HitNode variable to point to the TreeNode
// that the mouse is sitting on, if applicable. This is
// accomplished by the GetNodeAt function of the TreeView
// component. Our event provides us with the current mouse
// position ( x and y ), and the function will return a pointer
// to a TreeNode.
HitNode := tvwDemo.GetNodeAt( X, Y );
// Next, we are going to ensure that a node was actually hit.
// instead of the mouse sitting in empty space in the TreeView.
// For more complex checks, we can get a HitTests set. For
// the time being, this method will suffice. If no node
// is being hovered over by the mouse, GetNodeAt will return
// nil.
if not( HitNode = nil ) then
begin
// Next, we're going to make sure that the node in question
// has a parent. Since we're going to be handling dragging
// and dropping only with the nodes in this TreeView,
// there is no need to allow the dragging of the root node.
if not( HitNode.Parent = nil ) then
begin
// If all these conditions are met, then we'll begin the
// drag operation. This will set the cursor to the
// DragIcon property of the TreeView. BeginDrag takes
// a single parameter, a boolean that specifies whether
// the drag should begin immediately, or wait for the
// user to start moving the mouse with the button
// depressed. For standard Windows operation and in
// my opinion a much cleaner implementation, set this
// parameter to false.
tvwDemo.BeginDrag( false );
// In addition, we'll set the form-level variable
// FDragNode to our dragged TreeNode. We'll need to
// retrieve it for dropping and validation purposes.
FDragNode := HitNode;
end;
end;
end;
end;
procedure TfrmTreeViewDemo3.tvwDemoDragOver( Sender, Source:
TObject; X, Y: Integer; State: TDragState;
var Accept: Boolean );
// The tvwDemoDragOver event fires when a dragged object is moved over
// the TreeView component. At this point, we can do whatever
// processing necessary to validate the current drop target.
function HasAsAncestor( SourceNode, CheckedNode: TTreeNode ): boolean;
// This function declaration is scoped to the tvwDemoDragOver
// procedure and is not accessible from beyond the main
// procedure code. This is useful for purposes of
// encapsulation, as well as readibility of your interface
// section. Procedure scoping should be used when
// applicable, but make sure to plan ahead. If I think I may
// need this function outside of this procedure at some point
// in the future, it would be beneficial to instead place it in
// a code repository, or better yet extend the functionality of
// the TTreeNode component to handle this ability. We'll cover
// the customization of our TreeView component in a later
// tutorial.
// The HasAsAncestor function checks to see if SourceNode is a
// decendant of CheckedNode. This is neccessary to our
// validation process, and the purpose of the function will be
// discussed below.
begin
// We're going to default the result of this function to True.
// If our node passes all the tests, then at the end of the
// procedure we will set the Result to false.
Result := true;
// If the SourceNode is equal to or is the parent of
// CheckedNode, then we will return true.
if ( ( CheckedNode = SourceNode ) or ( SourceNode.Parent = CheckedNode ) ) then
begin
exit;
end;
// We will now move up the hierarchy of CheckedNode's ancestors,
// looking for a match with SourceNode. If one is found,
// then we will return true.
while not( CheckedNode.Parent = nil ) do
begin
if ( CheckedNode = SourceNode ) then
begin
exit;
end;
CheckedNode := CheckedNode.Parent;
end;
// If all conditions have been passed, then we will return
// false.
Result := false;
end;
var
// DropNode will point to the TreeNode that the mouse is
// hovering over.
DropNode: TTreeNode;
begin
// If our drag source is not a TTreeView, then we'll ignore this
// event.
if ( Source is TTreeView ) then
begin
// If it is, we're going to set DropNode to point to the
// TreeNode that is being dragged over.
DropNode := tvwDemo.GetNodeAt( X, Y );
// If we are not dragging over an actual node, then we'll ignore
// this event.
if not( DropNode = nil ) then
begin
// Ensure that the node being dropped is not a decendant of
// the dragged node. This way, nodes that are children,
// grandchildren, etc. of the dragged node are not valid
// drop targets. Accepting a drag like that could have
// some serious logical drawbacks.
Accept := not( HasAsAncestor( FDragNode, DropNode ) );
end;
end;
end;
procedure TfrmTreeViewDemo3.tvwDemoDragDrop(Sender, Source:
TObject; X, Y: Integer);
// The tvwDemoDragDrop procedure is fired when our dragged node is
// dropped onto a valid target.
var
// DropNode will point to the TreeNode that the mouse is hovering
// over.
DropNode: TTreeNode;
begin
// If our drag source is not a TTreeView, then we'll ignore this
// event.
if ( Source is TTreeView ) then
begin
// If it is, we're going to set DropNode to point to the
// TreeNode that is being dragged over.
DropNode := tvwDemo.GetNodeAt( X, Y );
// If a valid node is being dropped on, then we'll continue.
if not( DropNode = nil ) then
begin
// First, we'll fire off the MoveTo method of TTreeNode.
// This method takes two parameters... one is the node to
// move to, and the second is of type TNodeAttachMode.
// This is an enumeration allowing several different
// options for where the node being moved ends up in
// relation to the destination node. In our case,
// naAddChild will move the node to be the last child of
// the destination.
FDragNode.MoveTo( DropNode, naAddChild );
end;
end;
end;
procedure TfrmTreeViewDemo3.tvwDemoEndDrag(Sender, Target:
TObject; X, Y: Integer);
// The tvwDemoEndDrag procedure fires off when a drag is ended. This
// either happens when we manually end the drag ( with the EndDrag
// method ), or when the mouse button is released.
begin
// We want to clear our form-level variable once the drag has
// ended.
FDragNode := nil;
end;
As always, the source code is included in the component link at the top of this article.
For interested readers, the next part of the TreeView tutorial series will be dealing with loading information from a database, as well as a possibly different approach to hierarchal data storage and retrieval that proved much faster that a simple recursive function.