Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Дерево на базе MS SQL

01.01.2007
{ **** UBPFD *********** by delphibase.endimus.com ****
>> Дерево на базе MsSQL 7/2000 и DELPHI6 (BDE,ADO)
 
Узел дерева описывается через idParent,idPrior,idNext,idFirstChild.
В следствии такого подхода в многопользовательской среде достигается
минимальное количество блокировок при изменении узлов дерева.
Все функции реализованы в хранимых процедурах. Компанент, порожденный
от TTreeView, является интерфейсом для работы с деревом в клиенте.
Тексты хранимых процедур на странице
http://spenov.narod.ru/DBTree/DBTreeView.html
 
Зависимости: Classes,ComCtrls,CommCtrl,DB,DBTables,Controls,Messages,ADODB
Автор:       Пенов Сергей, spenov@narod.ru, ICQ:122597033, Москва
Copyright:   http://spenov.narod.ru/DBTree/DBTreeView.html
Дата:        6 сентября 2002 г.
***************************************************** }
 
//Тексты хранимых процедур на странице
// http://spenov.narod.ru/DBTree/DBTreeView.html
unit Un_TADODBTreeView;
 
interface
 
uses
  Classes, ComCtrls, CommCtrl, DB, DBTables, Controls, Messages, ADODB;
 
type
  TADODBTreeNode = class(TTreeNode)
  private
    FIdNode: Integer;
  public
    property idNode: Integer read FIdNode;
  end;
 
  TADODBTreeView = class(TCustomTreeView)
  private
    FRootID: string;
    FOnEdited: TTVEditedEvent;
    FLDblCklick: Boolean; //показывает, что выполняется DblClick
    FDoExpColOnDblClick: Boolean;
    //Если True, то при DblClick не будет раскрываться/закрываться Node.
    FReopenOnExpand: Boolean;
    FConnection: TADOConnection;
    FRecordset: _Recordset;
    FIdTree: Integer;
    procedure SetRootID(Value: string);
    procedure SetConnection(Value: TADOConnection);
    procedure SetIdTree(const Value: Integer);
    procedure AddChildren(AParent: TTreeNode);
    procedure WMLButtonDblClk(var Message: TWMLButtonDblClk); message
      WM_LBUTTONDBLCLK;
    function GetSelectedID: Integer;
    procedure SetSelectedID(const Value: Integer);
  protected
    procedure Loaded; override;
    function CreateNode: TTreeNode; override;
    function CanExpand(Node: TTreeNode): Boolean; override;
    function CanCollapse(Node: TTreeNode): Boolean; override;
    procedure DoEdited(Sender: TObject; Node: TTreeNode; var S: string);
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure dbLoadFirstLevel;
    function dbAddChild(AParent: TTreeNode; AText: string; idNode: Integer = 0):
      TTreeNode;
    procedure dbDeleteNode(Node: TTreeNode; ReQueryFromDB: Boolean = False);
    procedure dbMoveNode(DNode, SNode: TTreeNode; AsChild: Boolean = False;
      ReQueryFromDB: Boolean = False);
    property Items;
    property SelectedID: Integer read GetSelectedID write SetSelectedID;
  published
    property RootID: string read FRootID write SetRootID;
    property idDBTree: Integer read FIdTree write SetIdTree;
    property Connection: TADOConnection read FConnection write SetConnection;
    property DoExpColOnDblClick: Boolean read FDoExpColOnDblClick write
      FDoExpColOnDblClick default True;
    property OnEdited: TTVEditedEvent read FOnEdited write FOnEdited;
  published //Из TCustomTreeView
    property Align;
    property Anchors;
    property BevelEdges;
    property BevelInner;
    property BevelOuter;
    property BevelKind default bkNone;
    property BevelWidth;
    property BiDiMode;
    property BorderStyle;
    property BorderWidth;
    property ChangeDelay;
    property Color;
    property Ctl3D;
    property Constraints;
    property DragKind;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Font;
    property HideSelection;
    property HotTrack;
    property Images;
    property PopupMenu;
    property StateImages;
    property ReadOnly;
    property RightClickSelect;
    property RowSelect;
    property ShowButtons;
    property ShowHint;
    property ShowLines;
    property ShowRoot;
    property OnAddition;
    property OnAdvancedCustomDraw;
    property OnAdvancedCustomDrawItem;
    property OnChange;
    property OnChanging;
    property OnClick;
    property OnCollapsed;
    property OnCollapsing;
    property OnCompare;
    property OnContextPopup;
    property OnCreateNodeClass;
    property OnCustomDraw;
    property OnCustomDrawItem;
    property OnDblClick;
    property OnDeletion;
    property OnDragDrop;
    property OnDragOver;
    property OnEditing;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnExpanding;
    property OnExpanded;
    property OnGetImageIndex;
    property OnGetSelectedIndex;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
    //property Visible;
    { Items must be published after OnGetImageIndex and OnGetSelectedIndex }
    //property Items;
  end;
 
procedure Register;
 
implementation
 
uses
  SysUtils, Variants, Forms, DBLogDlg;
 
const
  SQLLoadLevel: string = 'EXEC upDBTreeGetChildren @idDBTree=%d,@idParent=%s';
  SQLAddChild: string =
  'EXEC upDBTreeAddNode @idDBTree=%d,@idParent=%s,@idPrior=%s,@idNext=%s,@Text=''%s'',@idNode=%s';
  SQLDeleteNode: string = 'EXEC upDBTreeDeleteNode @idDBTree=%d,@idNode=%d';
  SQLMoveNode: string =
  'EXEC upDBTreeMoveNode @idDBTree=%d,@idDNode=%d,@idSNode=%d,@AsChild=%d';
  SQLRenameNode: string =
  'EXEC upDBTreeRenameNode @idDBTree=%d,@idNode=%d,@NewText=''%s''';
  SQLGetFullPath: string = 'EXEC upDBTreeGetFullPath @idDBTree=%d,@idNode=%d';
 
procedure Register;
begin
  RegisterComponents('Penov', [TADODBTreeView]);
end;
 
{ TADODBTreeView }
 
procedure TADODBTreeView.AddChildren(AParent: TTreeNode);
var
  NewNode: TADODBTreeNode;
  TheCursor: TCursor;
  Buf: TTVExpandedEvent;
begin
  TheCursor := Screen.Cursor;
  Screen.Cursor := crHourGlass;
  try
    Buf := OnAddition;
    OnAddition := nil;
    try
      with FRecordset do
      begin
        if RecordCount > 0 then
          while not Eof do
          begin
            NewNode := Items.AddChild(AParent, Fields['Text'].Value) as
              TADODBTreeNode;
            with NewNode do
            begin
              HasChildren := not VarIsNull(Fields['idFirstChild'].Value);
              FIdNode := Fields['idNode'].Value;
            end;
            if Assigned(Buf) then
              Buf(Self, NewNode);
            MoveNext;
          end;
      end;
    finally
      OnAddition := Buf;
    end;
  finally
    Screen.Cursor := TheCursor;
  end;
end;
 
function TADODBTreeView.CanCollapse(Node: TTreeNode): Boolean;
begin
  if FLDblCklick and not FDoExpColOnDblClick then
    Result := False
  else
  begin
    Result := inherited CanCollapse(Node);
    //Удаление вложенных узлов
    if Result and FReopenOnExpand and (Node is TADODBTreeNode) and
      Node.HasChildren then
    begin
      Items.BeginUpdate;
      try
        Node.DeleteChildren;
        Items.AddChild(Node, 'HasItems');
      finally
        Items.EndUpdate;
      end;
    end;
  end;
end;
 
function TADODBTreeView.CanExpand(Node: TTreeNode): Boolean;
var
  crBuf: TCursor;
begin
  if FLDblCklick and not FDoExpColOnDblClick then
    Result := False
  else
  begin
    //Загрузка вложенных узлов
    if FReopenOnExpand and (Node is TADODBTreeNode) and Node.HasChildren then
    begin
      Items.BeginUpdate;
      try
        Node.DeleteChildren;
        if (FIdTree <> 0) and Assigned(FConnection) then
        begin
          crBuf := Screen.Cursor;
          Screen.Cursor := crSQLWait;
          try
            FRecordset := FConnection.Execute(Format(SQLLoadLevel, [FIdTree,
              IntToStr((Node as TADODBTreeNode).idNode)]));
          finally
            Screen.Cursor := crBuf;
          end;
          try
            AddChildren(Node);
          finally
            FRecordset := nil;
          end;
        end;
      finally
        Items.EndUpdate;
      end;
    end;
    Result := inherited CanExpand(Node);
  end;
end;
 
constructor TADODBTreeView.Create(AOwner: TComponent);
begin
  FRootID := 'NULL';
  FReopenOnExpand := True;
  FDoExpColOnDblClick := True;
  inherited;
  inherited OnEdited := DoEdited;
end;
 
function TADODBTreeView.CreateNode: TTreeNode;
begin
  if Assigned(OnCreateNodeClass) then
    Result := inherited CreateNode
  else
    Result := TADODBTreeNode.Create(Items);
end;
 
function TADODBTreeView.dbAddChild(AParent: TTreeNode; AText: string; idNode:
  Integer = 0): TTreeNode;
var
  NewNode: TTreeNode;
  Buf: TTVExpandedEvent;
  crBuf: TCursor;
 
  function GetIdParent(Node: TTreeNode): string;
  begin
    if Assigned(Node.Parent) then
      Result := IntToStr((Node.Parent as TADODBTreeNode).idNode)
    else
      Result := FRootID;
  end;
  function GetIdPrior(Node: TTreeNode): string;
  var
    Prior: TTreeNode;
  begin
    Prior := Node.getPrevSibling;
    if Assigned(Prior) then
      Result := IntToStr((Prior as TADODBTreeNode).idNode)
    else
      Result := 'NULL';
  end;
  function GetIdNext(Node: TTreeNode): string;
  var
    Next: TTreeNode;
  begin
    Next := Node.getNextSibling;
    if Assigned(Next) then
      Result := IntToStr((Next as TADODBTreeNode).idNode)
    else
      Result := 'NULL';
  end;
  function GetIdNode(idNode: Integer): string;
  begin
    if idNode <> 0 then
      Result := IntToStr(idNode)
    else
      Result := 'NULL';
  end;
 
begin
  Result := nil;
  Buf := OnAddition;
  OnAddition := nil;
  try
    Items.BeginUpdate;
    try
      if Assigned(AParent) and not AParent.Expanded then
        AParent.Expand(False);
      NewNode := Items.AddChild(AParent, AText);
      if (FIdTree <> 0) and Assigned(FConnection) then
      begin
        crBuf := Screen.Cursor;
        Screen.Cursor := crSQLWait;
        try
          FRecordset := FConnection.Execute(Format(SQLAddChild, [FIdTree,
            GetIdParent(NewNode), GetIdPrior(NewNode), GetIdNext(NewNode),
              AText,
              GetIdNode(idNode)]));
        finally
          Screen.Cursor := crBuf;
        end;
        try
          try
            if FRecordset.RecordCount > 0 then
            begin
              (NewNode as TADODBTreeNode).FIdNode :=
                FRecordset.Fields['NewId'].Value;
              //Выделяем добавленный узел
              FReopenOnExpand := False;
              try
                Selected := NewNode;
              finally
                FReopenOnExpand := True;
              end;
            end
            else
              raise
                Exception.Create('TADODBTreeView.dbAddChild:Не получен идентификатор нового узла.');
          except
            NewNode.Delete;
            raise;
          end;
        finally
          FRecordset := nil;
        end;
      end;
    finally
      Items.EndUpdate;
    end;
    Result := NewNode;
    if Assigned(Buf) then
      Buf(Self, NewNode);
  finally
    OnAddition := Buf;
  end;
end;
 
procedure TADODBTreeView.dbDeleteNode(Node: TTreeNode; ReQueryFromDB: Boolean =
  False);
var
  AParent: TTreeNode;
begin
  if Node.HasChildren then
    raise
      Exception.Create('TADODBTreeView.dbDeleteNode:Этот узел удалить нельзя,т.к. есть вложеннные узлы.');
  FConnection.Execute(Format(SQLDeleteNode, [FIdTree, (Node as
      TADODBTreeNode).idNode]));
  if ReQueryFromDB then
  begin
    Items.BeginUpdate;
    try
      AParent := Node.Parent;
      if Assigned(AParent) then
      begin
        AParent.Collapse(False);
        AParent.Expand(False);
      end
      else
        dbLoadFirstLevel;
    finally
      Items.EndUpdate;
    end;
  end
  else
    Node.Delete;
end;
 
procedure TADODBTreeView.dbMoveNode(DNode, SNode: TTreeNode; AsChild: Boolean =
  False; ReQueryFromDB: Boolean = False);
const
  BoolToInt: array[Boolean] of Integer = (0, 1);
var
  DParent, SParent, Node: TTreeNode;
  TheNodeId: Integer;
begin
  if not Assigned(DNode) or not Assigned(SNode) or (DNode = SNode) then
    Exit;
  if DNode.HasAsParent(SNode) then
    raise
      Exception.Create('TADODBTreeView.dbMoveNode:Узел не может быть перемещен.')
  else
  begin
    FConnection.Execute(Format(SQLMoveNode, [FIdTree, (DNode as
        TADODBTreeNode).idNode, (SNode as TADODBTreeNode).idNode,
      BoolToInt[AsChild]]));
    Items.BeginUpdate;
    try
      if ReQueryFromDB then
      begin
        TheNodeId := (SNode as TADODBTreeNode).idNode;
        DParent := DNode.Parent;
        SParent := SNode.Parent;
        if Assigned(DParent) and Assigned(SParent) then
        begin
          DParent.Collapse(False);
          DParent.Expand(False);
          if (DParent <> SParent) and not SParent.HasAsParent(DParent) then
          begin
            DParent.Collapse(False);
            DParent.Expand(False);
          end;
        end
        else
          dbLoadFirstLevel;
        if Assigned(DParent) then
          Node := DParent.getFirstChild
        else
          Node := Items.GetFirstNode;
        while Assigned(Node) and ((Node as TADODBTreeNode).idNode <> TheNodeId)
          do
          Node := Node.getNextSibling;
        if Assigned(Node) then
          Selected := Node;
      end
      else
      try
        if AsChild then
        begin
          if DNode.Expanded then
          begin
            FReopenOnExpand := False;
            SNode.MoveTo(DNode, naAddChild);
          end
          else
          begin
            Items.AddChildFirst(DNode, 'HasChildren');
            //Надо добавить узел,что бы DNode открылся.
            if CanExpand(DNode) then
            begin
              SNode.Delete;
              FReopenOnExpand := False;
              DNode.GetLastChild.Selected := True;
            end;
          end;
        end
        else
        begin
          FReopenOnExpand := False;
          SNode.MoveTo(DNode, naInsert);
        end;
      finally
        FReopenOnExpand := True;
      end;
    finally
      Items.EndUpdate;
    end;
  end;
end;
 
procedure TADODBTreeView.Loaded;
begin
  inherited;
  if not (csDesigning in ComponentState) then
    dbLoadFirstLevel;
end;
 
procedure TADODBTreeView.dbLoadFirstLevel;
var
  crBuf: TCursor;
begin
  Items.Clear;
  if not (csDesigning in Self.ComponentState) and not (csLoading in
    Self.ComponentState) and (FIdTree <> 0) and Assigned(FConnection) then
  begin
    crBuf := Screen.Cursor;
    Screen.Cursor := crSQLWait;
    try
      FRecordset := FConnection.Execute(Format(SQLLoadLevel, [FIdTree,
        FRootID]));
    finally
      Screen.Cursor := crBuf;
    end;
    try
      AddChildren(nil);
    finally
      FRecordset := nil;
    end;
  end;
end;
 
procedure TADODBTreeView.SetConnection(Value: TADOConnection);
begin
  if Assigned(FConnection) and (FConnection.Owner <> Self.Owner) then
    FConnection.RemoveFreeNotification(Self);
  FConnection := Value;
  if Assigned(Value) then
  begin
    if Value.Owner <> Self.Owner then
      Value.FreeNotification(Self);
    dbLoadFirstLevel;
  end
  else
    Items.Clear;
end;
 
procedure TADODBTreeView.SetIdTree(const Value: Integer);
begin
  FIdTree := Value;
  dbLoadFirstLevel;
end;
 
procedure TADODBTreeView.WMLButtonDblClk(var Message: TWMLButtonDblClk);
begin
  FLDblCklick := True;
  inherited;
  FLDblCklick := False;
end;
 
function TADODBTreeView.GetSelectedID: Integer;
begin
  if Assigned(Selected) and (Selected is TADODBTreeNode) then
    Result := (Selected as TADODBTreeNode).idNode
  else
    Result := 0;
end;
 
procedure TADODBTreeView.SetSelectedID(const Value: Integer);
var
  TheNode: TTreeNode;
  ThePath: array of Integer;
  I: Integer;
  crBuf: TCursor;
begin
  if (Items.Count > 0) and (Items[0] is TADODBTreeNode) then
  begin
    Items.BeginUpdate;
    try
      try
        TheNode := Items[0];
        crBuf := Screen.Cursor;
        Screen.Cursor := crSQLWait;
        try
          FRecordset := FConnection.Execute(Format(SQLGetFullPath, [FIdTree,
            Value]));
        finally
          Screen.Cursor := crBuf;
        end;
        try
          if FRecordset.RecordCount <= 0 then
            raise
              Exception.Create('TADODBTreeView.SetSelectedID:Не получен путь к узлу ' + IntToStr(Value));
          SetLength(ThePath, FRecordset.RecordCount);
          I := 0;
          while not FRecordset.Eof do
          begin
            ThePath[I] := FRecordset.Fields['idNode'].Value;
            Inc(I);
            FRecordset.MoveNext;
          end;
        finally
          FRecordset := nil;
        end;
        for I := 0 to High(ThePath) do
        begin
          while Assigned(TheNode) and ((TheNode as TADODBTreeNode).idNode <>
            ThePath[I]) do
            TheNode := TheNode.getNextSibling;
          if not Assigned(TheNode) then
            raise Exception.Create('TADODBTreeView.SetSelectedID:Не найден узел '
              + IntToStr(ThePath[I]));
          if I < High(ThePath) then
          begin
            TheNode.Expand(False);
            TheNode := TheNode.getFirstChild;
          end;
        end;
        if not Assigned(TheNode) then
          raise
            Exception.Create('TADODBTreeView.SetSelectedID:Не найден узел.');
        Selected := TheNode;
      finally
        ThePath := nil;
      end;
    finally
      Items.EndUpdate;
    end;
  end;
end;
 
{ TADODBTreeNode }
 
procedure TADODBTreeView.DoEdited(Sender: TObject; Node: TTreeNode; var S:
  string);
var
  crBuf: TCursor;
begin
  if Assigned(FOnEdited) then
    FOnEdited(Sender, Node, S);
  if (Node is TADODBTreeNode) and (Node.Text <> S) then
  try //Сохраняем изменения в базе
    crBuf := Screen.Cursor;
    Screen.Cursor := crSQLWait;
    try
      FRecordset := FConnection.Execute(Format(SQLRenameNode, [FIdTree, (Node as
          TADODBTreeNode).idNode, S]));
    finally
      Screen.Cursor := crBuf;
    end;
    try
      if FRecordset.RecordCount = 0 then
        raise
          Exception.Create('TADODBTreeView.DoEdited:Не получен результат переименования.');
      S := FRecordset.Fields['NewText'].Value;
    finally
      FRecordset := nil;
    end;
  except
    S := Node.Text;
    raise;
  end;
end;
 
procedure TADODBTreeView.SetRootID(Value: string);
var
  I: Integer;
begin
  if (UpperCase(Value) = 'NULL') or (Value = '') then
    FRootID := 'NULL'
  else
  begin
    for I := 1 to Length(Value) do
      if not (Value[I] in ['0'..'9']) then
        raise Exception.Create('"' + Value + '" - is not integer or NULL');
    FRootID := Value;
  end;
  dbLoadFirstLevel;
end;
 
procedure TADODBTreeView.Notification(AComponent: TComponent; Operation:
  TOperation);
begin
  if (Operation = opRemove) and (AComponent = FConnection) then
    SetConnection(nil);
end;
 
{ TADODBTreeNode }
 
end.