{ *******************************************************

  TreeViewManage
  作者:不懂

  属性:
  TreeView:需要实现拖拽功能的 TreeView,当把一个 TreeView
  指定给该属性后,这个 TreeView 的节点就具有智能拖拽功能了。
  DragMode = dmHotKeyDrag   // 通过快捷键才能拖拽
  HotKeyMoveNode = hkCtrl;  // 拖拽节点: Ctrl
  HotKeyCopyNode = hkShift; // 拖拽并复制节点:Shift
  HotKeyChildNode = hkAlt;  // 拖拽到子节点:Alt
  EnableRButtonDrag = True; // 允许右键拖拽,会弹出菜单

  Public 方法:
  AddNode:添加节点,根据 AddMode 决定添加的位置
  DeleteNode:删除节点,返回被删除节点临近的节点
  MoveNode:移动或复制节点,根据 MoveMode 决定移动方式

******************************************************* }

unit TreeViewManage;

interface

uses
  SysUtils, Windows, Classes, Controls, ComCtrls, Menus;

type
  TAttachMode = (amLast, amFirst, amChildLast, amChildFirst, amPrev,
    amNext, amAuto);

  { 控制拖拽方式的热键:禁止,Ctrl,Shift,Alt }
  THotKey = (hkNone, hkCtrl, hkShift, hkAlt);

  { 节点拖动方式:自动拖拽,热键拖拽,禁止拖拽 }
  TDragMode = (dmAutoDrag, dmHotKeyDrag, dmDisableDrag);

  TTreeViewDrager = class(TComponent)
  private
    FTreeView: TTreeView;
    FOldOnMouseDown: TMouseEvent;
    FOldOnMouseUp: TMouseEvent;
    FOldOnDragOver: TDragOverEvent;
    FOldOnDragDrop: TDragDropEvent;

    FDragMode: TDragMode; { 节点拖动方式 }
    FDragButton: TMouseButton; { 拖动节点的按钮 }
    FDropMenu: TPopupMenu; { 右键拖拽后的弹出菜单 }
    FMoveSourceNode: TTreeNode; { 移动的源节点 }
    FMoveTargetNode: TTreeNode; { 移动的目标节点 }
    FHotKeyMoveNode: Integer; { 拖动节点的热键 }
    FHotKeyCopyNode: Integer; { 复制节点的热键 }
    FHotKeyChildNode: Integer; { 拖动到子节点的热键 }
    FEnableRButtonDrag: Boolean; { 是否允许右键拖拽,右键拖拽会弹出菜单 }

    function GetTreeView: TCustomTreeView;
    procedure SetTreeView(Value: TCustomTreeView);
    function GetHotKeyMoveNode: THotKey;
    procedure SetHotKeyMoveNode(Key: THotKey);
    function GetHotKeyCopyNode: THotKey;
    procedure SetHotKeyCopyNode(Key: THotKey);
    function GetHotKeyChildNode: THotKey;
    procedure SetHotKeyChildNode(Key: THotKey);

    procedure MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer);
    procedure DragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure DragDrop(Sender, Source: TObject; X, Y: Integer);
  protected
    function CreateDropMenu: TPopupMenu; virtual;
    procedure DragMenuEvent(Sender: TObject); virtual;
    function GetNewNode(RelativeNode: TTreeNode = nil; NodeName: string = '';
      AddMode: TAttachMode = amAuto): TTreeNode;
    function CloneNode(FromNode, ToNode: TTreeNode;
      MoveMode: TAttachMode = amAuto): TTreeNode;
    procedure CopyChildNodes(FromNode, ToNode: TTreeNode); virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    function AddNode(RelativeNode: TTreeNode = nil; NodeName: string = '';
      AddMode: TAttachMode = amAuto): TTreeNode; virtual;
    function DeleteNode(RelativeNode: TTreeNode): TTreeNode; virtual;
    function MoveNode(FromNode, ToNode: TTreeNode;
      MoveMode: TAttachMode = amAuto; bCopy: Boolean = False)
      : TTreeNode; virtual;
  published
    property TreeView: TCustomTreeView read GetTreeView Write SetTreeView;
    property DragMode: TDragMode read FDragMode Write FDragMode
      default dmHotKeyDrag;
    property HotKeyMoveNode: THotKey read GetHotKeyMoveNode
      write SetHotKeyMoveNode default hkCtrl;
    property HotKeyCopyNode: THotKey read GetHotKeyCopyNode
      write SetHotKeyCopyNode default hkShift;
    property HotKeyChildNode: THotKey read GetHotKeyChildNode
      write SetHotKeyChildNode default hkAlt;
    property EnableRButtonDrag: Boolean read FEnableRButtonDrag
      write FEnableRButtonDrag default True;
  end;

const
  { 由于 Delphi 的 TreeView 所能管理的最大节点数为 65535,所以这里给出范围限制 }
  MaxNodeCount = 65535;

resourcestring
  Error_NodeOutOfRange = '警告:TreeView 节点数达到最大限制:%d,无法继续添加节点';

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TTreeViewDrager]);
end;

{ 判断按键是否被按下 }
function IsKeyDown(VK: Integer): Boolean;
begin
  Result := GetKeyState(VK) < 0;
end;

constructor TTreeViewDrager.Create(AOwner: TComponent);
begin
  inherited;
  FDragMode := dmHotKeyDrag;
  HotKeyMoveNode := hkCtrl;
  HotKeyCopyNode := hkShift;
  HotKeyChildNode := hkAlt;
  FDropMenu := CreateDropMenu;
  FEnableRButtonDrag := True;
end;

destructor TTreeViewDrager.Destroy;
begin
  FDropMenu.Free;
  inherited;
end;

{ ------------------------------------------------------------ }
{ 拖放后的弹出菜单 }
{ ------------------------------------------------------------ }

function TTreeViewDrager.CreateDropMenu: TPopupMenu;
const
  DropMenuName: array [1 .. 9] of PChar = ('移动到之前(&1)', '移动到之后(&2)',
    '移动到子节点最前(&5)', '移动到子节点最后(&6)', '-', '复制到之前(&A)', '复制到之后(&B)',
    '复制到子节点最前(&E)', '复制到子节点最后(&F)');
var
  I: Integer;
  NewItem: TMenuItem;
begin
  Result := TPopupMenu.Create(FTreeView);

  for I := Low(DropMenuName) to High(DropMenuName) do
  begin
    NewItem := TMenuItem.Create(FTreeView);
    NewItem.Tag := I;
    NewItem.OnClick := DragMenuEvent;
    NewItem.Caption := DropMenuName[I];
    Result.Items.Add(NewItem);
  end;
end;

procedure TTreeViewDrager.DragMenuEvent(Sender: TObject);
const
  MoveMode: array [1 .. 4] of TAttachMode = (amPrev, amNext, amChildFirst,
    amChildLast);
var
  bCopy: Boolean;
  Index: Integer;
  TargetNode: TTreeNode;
begin
  if FMoveSourceNode = nil then
    Exit;

  Index := (Sender as TMenuItem).Tag;

  if Index > (FDropMenu.Items.Count div 2 + 1) then
  begin
    Index := Index - (FDropMenu.Items.Count div 2 + 1);
    bCopy := True;
  end
  else
    bCopy := False;

  if (FMoveSourceNode = FMoveTargetNode) and (Index in [3, 4]) then
    Exit;

  TargetNode := MoveNode(FMoveSourceNode, FMoveTargetNode,
    MoveMode[Index], bCopy);
  if TargetNode <> nil then
    TargetNode.Selected := True
end;

{ ------------------------------------------------------------ }
{ 属性相关 }
{ ------------------------------------------------------------ }

function TTreeViewDrager.GetTreeView: TCustomTreeView;
begin
  Result := TCustomTreeView(FTreeView);
end;

procedure TTreeViewDrager.SetTreeView(Value: TCustomTreeView);
begin
  if FTreeView <> Value then
  begin
    FTreeView := TTreeView(Value);
    { 不能设置 TCustomTreeVIew 的 RightClickSelect 为 True
      否则右键单击会错误触发拖拽操作 }
    FTreeView.RightClickSelect := False;
    FTreeView.DragMode := dmManual;
    FOldOnMouseDown := FTreeView.OnMouseDown;
    FOldOnMouseUp := FTreeView.OnMouseUp;
    FOldOnDragOver := FTreeView.OnDragOver;
    FOldOnDragDrop := FTreeView.OnDragDrop;
    FTreeView.OnMouseDown := MouseDown;
    FTreeView.OnMouseUp := MouseUp;
    FTreeView.OnDragOver := DragOver;
    FTreeView.OnDragDrop := DragDrop;
  end;
end;

function GetCtrlKey(VirtualKey: Integer): THotKey;
begin
  case VirtualKey of
    VK_CONTROL:
      Result := hkCtrl;
    VK_MENU:
      Result := hkAlt;
    VK_SHIFT:
      Result := hkShift;
  else
    Result := hkNone;
  end;
end;

function GetVirtualKey(CtrlKey: THotKey): Integer;
begin
  case CtrlKey of
    hkCtrl:
      Result := VK_CONTROL;
    hkAlt:
      Result := VK_MENU;
    hkShift:
      Result := VK_SHIFT;
  else
    Result := 0;
  end;
end;

function TTreeViewDrager.GetHotKeyMoveNode: THotKey;
begin
  Result := GetCtrlKey(FHotKeyMoveNode);
end;

procedure TTreeViewDrager.SetHotKeyMoveNode(Key: THotKey);
begin
  FHotKeyMoveNode := GetVirtualKey(Key);
end;

function TTreeViewDrager.GetHotKeyCopyNode: THotKey;
begin
  Result := GetCtrlKey(FHotKeyCopyNode);
end;

procedure TTreeViewDrager.SetHotKeyCopyNode(Key: THotKey);
begin
  FHotKeyCopyNode := GetVirtualKey(Key);
end;

function TTreeViewDrager.GetHotKeyChildNode: THotKey;
begin
  Result := GetCtrlKey(FHotKeyChildNode);
end;

procedure TTreeViewDrager.SetHotKeyChildNode(Key: THotKey);
begin
  FHotKeyChildNode := GetVirtualKey(Key);
end;

{ ------------------------------------------------------------ }
{ 非公开方法 }
{ ------------------------------------------------------------ }

{ 添加新节点:供 AddNode 和 MoveNode 调用,避免各个 Pbulic 方法之间相互调用 }
function TTreeViewDrager.GetNewNode(RelativeNode: TTreeNode = nil;
  NodeName: string = ''; AddMode: TAttachMode = amAuto): TTreeNode;
var
  NextNode: TTreeNode;
  NodeAddMode: TNodeAttachMode;
begin
  if FTreeView.Items.Count = MaxNodeCount then
  begin
    MessageBox(FTreeView.Handle, PChar(Format(Error_NodeOutOfRange,
      [MaxNodeCount])), '', MB_OK + MB_ICONERROR);
    Result := nil;
    Exit;
  end
  else
  begin
    { 这里 amAuto 当 amNext 处理 }
    if AddMode = amAuto then
      AddMode := amNext;

    { 转换 AddMode 为 NodeAddMode }
    case AddMode of
      amLast .. amPrev:
        NodeAddMode := TNodeAttachMode(AddMode);
      amNext:
        begin
          if RelativeNode = nil then
            NodeAddMode := naAdd
          else
          begin
            NextNode := RelativeNode.GetNextSibling;
            if NextNode <> nil then
            begin
              RelativeNode := NextNode;
              NodeAddMode := naInsert;
            end
            else
              NodeAddMode := naAdd;
          end
        end;
    else
      NodeAddMode := naAdd;
    end;
    Result := FTreeView.Items.AddNode(nil, RelativeNode, NodeName, nil,
      NodeAddMode);
  end;
end;

{ 克隆节点,供 MoveNode 调用 }
function TTreeViewDrager.CloneNode(FromNode, ToNode: TTreeNode;
  MoveMode: TAttachMode = amAuto): TTreeNode;
begin
  if FromNode = ToNode then
    MoveMode := amNext;

  { 这里 amAuto 根据上移下移来决定移动方式 }
  if MoveMode = amAuto then
  begin
    if ToNode = nil then
      MoveMode := amFirst
    else if FromNode.Parent = ToNode.Parent then
    begin
      { 同级节点,根据移动的方向决定是移到前面还是移到后面 }
      if FromNode.Index &gt; ToNode.Index then
        MoveMode := amPrev
      else
        MoveMode := amNext;
    end
    else
      { 不同级节点,移到后面 }
      MoveMode := amNext;
  end;

  Result := GetNewNode(ToNode, FromNode.Text, MoveMode);
  // Result.Data := FromNode.Data;
end;

{ 复制子节点,供 MoveNode 调用 }
procedure TTreeViewDrager.CopyChildNodes(FromNode, ToNode: TTreeNode);
var
  I: Integer;
  NewNode: TTreeNode;
begin
  if (FromNode = nil) or (ToNode = nil) then
    Exit;

  for I := 0 to FromNode.Count - 1 do
  begin
    NewNode := GetNewNode(ToNode, FromNode[I].Text, amChildLast);
    // NewNode.Data := FromNode[I].Data;
    if NewNode = nil then
      Exit;
    if FromNode[I].Count &gt; 0 then
      CopyChildNodes(FromNode[I], NewNode);
  end;
end;

{ ------------------------------------------------------------ }
{ 公开方法 }
{ ------------------------------------------------------------ }

{ 添加新节点 }
function TTreeViewDrager.AddNode(RelativeNode: TTreeNode = nil;
  NodeName: string = ''; AddMode: TAttachMode = amAuto): TTreeNode;
begin
  Result := GetNewNode(RelativeNode, NodeName, AddMode);
end;

{ 删除节点 }
function TTreeViewDrager.DeleteNode(RelativeNode: TTreeNode): TTreeNode;
begin
  if RelativeNode = nil then
  begin
    Result := nil;
    Exit;
  end;

  Result := RelativeNode.GetNextSibling;
  if Result = nil then
    Result := RelativeNode.GetPrevSibling;
  if Result = nil then
    Result := RelativeNode.Parent;
  RelativeNode.Delete;
end;

{ 移动节点 }
function TTreeViewDrager.MoveNode(FromNode, ToNode: TTreeNode;
  MoveMode: TAttachMode = amAuto; bCopy: Boolean = False): TTreeNode;
var
  NextNode: TTreeNode;
  NodeAddMode: TNodeAttachMode;
begin
  Result := FromNode;

  { 不能移动到自身的子节点中 }
  if (FromNode = ToNode) and (MoveMode in [amChildFirst, amChildLast]) then
    Exit;

  FTreeView.Items.BeginUpdate;
  try
    { 这里 amAuto 根据上移下移来决定移动方式 }
    if MoveMode = amAuto then
    begin
      if ToNode = nil then
        MoveMode := amFirst
      else if FromNode.Parent = ToNode.Parent then
      begin
        { 同级节点,根据移动的方向决定是移到前面还是移到后面 }
        if FromNode.Index &gt; ToNode.Index then
          MoveMode := amPrev
        else
          MoveMode := amNext;
      end
      else
        { 不同级节点,移到后面 }
        MoveMode := amNext;
    end;

    if bCopy then
    begin
      Result := GetNewNode(ToNode, FromNode.Text, MoveMode);
      if Result <> nil then
        CopyChildNodes(FromNode, Result);
    end
    else
    begin
      case MoveMode of
        amLast .. amPrev:
          NodeAddMode := TNodeAttachMode(MoveMode);
        amNext:
          begin
            NextNode := ToNode.GetNextSibling;
            if NextNode <> nil then
            begin
              ToNode := NextNode;
              NodeAddMode := naInsert;
            end
            else
              NodeAddMode := naAdd;
          end;
      else
        NodeAddMode := naAdd;
      end;

      Result := FromNode;
      FromNode.MoveTo(ToNode, NodeAddMode);
    end;
  finally
    FTreeView.Items.EndUpdate;
  end;
end;

{ ------------------------------------------------------------ }
{ 实现拖拽 }
{ ------------------------------------------------------------ }

{ 准备拖拽 }
procedure TTreeViewDrager.MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Assigned(FOldOnMouseDown) then
    FOldOnMouseDown(Sender, Button, Shift, X, Y);

  if FDragMode = dmDisableDrag then
    Exit;

  { 判断鼠标是否点击在节点上 }
  if (htOnItem in FTreeView.GetHitTestInfoAt(X, Y)) then
  begin
    { 强行许右键选择节点,忽略 RightClickSelect 属性 }
    if (Button = mbRight) then
      FTreeView.GetNodeAt(X, Y).Selected := True;

    { 判断是否满足拖拽条件 }
    if (FDragMode = dmAutoDrag) or IsKeyDown(FHotKeyMoveNode) or
      IsKeyDown(FHotKeyCopyNode) or IsKeyDown(FHotKeyChildNode) then
    begin
      FDragButton := Button;
      { 左右键均可拖拽 }
      if (Button = mbLeft) or (Button = mbRight) then
        { Immediate = True 则拖拽操作会立刻开始
          Immediate = False 当达到 Threshold 设定的值时,才会产生拖拽操作 }
        FTreeView.BeginDrag(False); { 启用拖拽 }
    end;
  end;
end;

{ 取消拖拽:如果不取消拖拽,则鼠标右键单击后,会进入拖拽状态,再次单击才退出 }
procedure TTreeViewDrager.MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Assigned(FOldOnMouseUp) then
    FOldOnMouseUp(Sender, Button, Shift, X, Y);
  if FTreeView.Dragging then
    FTreeView.EndDrag(False);
end;

{ 接受拖拽 }
procedure TTreeViewDrager.DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  Accept := False;
  if Assigned(FOldOnDragOver) then
    FOldOnDragOver(Sender, Source, X, Y, State, Accept);

  if FDragMode = dmDisableDrag then
    Exit;

  FMoveSourceNode := FTreeView.Selected;
  FMoveTargetNode := FTreeView.GetNodeAt(X, Y);
  { 必须在同一个 TreeView 内部拖拽,目标不能为 nil }
  if (Source = FTreeView) and (FMoveTargetNode <> nil) then
  begin
    { 源节点不能为目标节点的父节点 }
    if not FMoveTargetNode.HasAsParent(FMoveSourceNode) then
      Accept := True;
  end;
end;

{ 完成拖拽 }
procedure TTreeViewDrager.DragDrop(Sender, Source: TObject; X, Y: Integer);
var
  CurPos: TPoint;
  bCopy: Boolean;
  MoveMode: TAttachMode;
begin
  if Assigned(FOldOnDragDrop) then
    FOldOnDragDrop(Sender, Source, X, Y);

  if FDragMode = dmDisableDrag then
    Exit;

  if FDropMenu <> nil then
    if FDragButton = mbRight then
    begin
      CurPos.X := X;
      CurPos.Y := Y;
      CurPos := FTreeView.ClientToScreen(CurPos);
      FDropMenu.Popup(CurPos.X, CurPos.Y);
    end
    else
    begin
      if IsKeyDown(FHotKeyChildNode) then
        MoveMode := amChildLast
      else
        MoveMode := amAuto;
      bCopy := IsKeyDown(FHotKeyCopyNode);
      MoveNode(FMoveSourceNode, FMoveTargetNode, MoveMode, bCopy)
        .Selected := True;
    end;
end;

end.



{ *******************************************************
  使用举例:创建一个空白窗体程序,双击窗体,使用如下代码
******************************************************* }

procedure TForm1.FormCreate(Sender: TObject);
var
  I: Integer;
  tv1: TTreeView;
  tvd1: TTreeViewDrager;
begin
  { 创建 TreeView,也可以在窗体设计器中创建 }
  tv1 := TTreeView.Create(Self);
  tv1.Parent := Self;
  tv1.Align := alClient;
  for I := 1 to 10 do
    tv1.Items.Add(nil, IntToStr(I));
  { 创建 TreeViewDrager,也可以将 TreeViewDrager 安装为 Delphi 组件 }
  { 然后在窗体设计器中创建 }
  tvd1 := TTreeViewDrager.Create(Self);
  tvd1.TreeView := tv1;
  // { 将 HotKeyCopyNode 设置为 hkNone 表示禁止通过拖拽方式复制节点 }
  // tvd1.HotKeyCopyNode := hkNone;
end;



{ 很久以前写过一个这样的控件,结果弄丢了,现在重写一个,希望大家编写的软件能更加友好 }