Карта сайта Kansoftware
НОВОСТИУСЛУГИРЕШЕНИЯКОНТАКТЫ
KANSoftWare

Реализация TreeView с чекбоксами и частичным выделением в Delphi 12.2

Delphi , Компоненты и Классы , TTreeView

 

Введение

При работе с древовидными структурами в Delphi часто возникает необходимость организовать сложную логику взаимодействия с чекбоксами узлов. В этой статье мы рассмотрим, как реализовать TreeView с двухуровневой структурой, где родительские узлы автоматически отмечаются при выборе всех дочерних элементов и отображают частичное выделение, когда выбраны только некоторые дочерние элементы.

Основные требования

Нам необходимо реализовать следующее поведение:

  1. При клике на родительский узел:
    Он отмечается, и все его дочерние элементы также отмечаются
    При повторном клике - снимается выделение у родителя и всех дочерних элементов
  2. При клике на дочерний узел:
    Узел отмечается
    Если все дочерние элементы отмечены - родитель отмечается полностью
    Если некоторые дочерние элементы отмечены - родитель получает частичное выделение

  3. При повторном клике - узел снимает выделение и обновляет состояние родителя

Настройка компонента TreeView

Для начала настроим базовые свойства TreeView:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  TreeView1.CheckBoxes := True;
  TreeView1.CheckStyles := [csTriStateCheckBox, csPartial];

  // Добавляем тестовые данные
  var ParentNode := TreeView1.Items.Add(nil, 'Родительский узел 1');
  TreeView1.Items.AddChild(ParentNode, 'Дочерний узел 1.1');
  TreeView1.Items.AddChild(ParentNode, 'Дочерний узел 1.2');

  ParentNode := TreeView1.Items.Add(nil, 'Родительский узел 2');
  TreeView1.Items.AddChild(ParentNode, 'Дочерний узел 2.1');
  TreeView1.Items.AddChild(ParentNode, 'Дочерний узел 2.2');
end;

Реализация логики изменения состояний

Основная сложность заключается в том, что при включенном csPartial чекбоксы начинают циклически переключаться между тремя состояниями, а нам нужно только между двумя. Решение - использовать события OnCheckStateChanging и OnCheckStateChanged.

type
  TMainForm = class(TForm)
    TreeView1: TTreeView;
    procedure TreeView1CheckStateChanging(Sender: TCustomTreeView;
      Node: TTreeNode; NewCheckState, OldCheckState: TNodeCheckState;
      var AllowChange: Boolean);
    procedure TreeView1CheckStateChanged(Sender: TCustomTreeView;
      Node: TTreeNode; CheckState: TNodeCheckState);
  private
    { Private declarations }
    FUpdatingStates: Boolean;
    procedure UpdateChildNodes(ParentNode: TTreeNode; NewState: TNodeCheckState);
    procedure UpdateParentState(ChildNode: TTreeNode);
  public
    { Public declarations }
  end;

implementation

procedure TMainForm.TreeView1CheckStateChanging(Sender: TCustomTreeView;
  Node: TTreeNode; NewCheckState, OldCheckState: TNodeCheckState;
  var AllowChange: Boolean);
begin
  // Запрещаем переход в состояние частичного выделения при клике пользователя
  if (NewCheckState = ncsPartial) and (not FUpdatingStates) then
  begin
    AllowChange := False;
    // Переключаем между checked и unchecked
    if OldCheckState = ncsUnchecked then
      Node.CheckState := ncsChecked
    else
      Node.CheckState := ncsUnchecked;
  end;
end;

procedure TMainForm.TreeView1CheckStateChanged(Sender: TCustomTreeView;
  Node: TTreeNode; CheckState: TNodeCheckState);
begin
  if FUpdatingStates then Exit;

  FUpdatingStates := True;
  try
    // Обрабатываем только явные checked/unchecked состояния
    case CheckState of
      ncsChecked, ncsUnchecked:
        if Node.Level = 0 then
          UpdateChildNodes(Node, CheckState)  // Родительский узел - обновляем детей
        else
          UpdateParentState(Node);            // Дочерний узел - обновляем родителя
    end;
  finally
    FUpdatingStates := False;
  end;
end;

procedure TMainForm.UpdateChildNodes(ParentNode: TTreeNode; NewState: TNodeCheckState);
begin
  // Устанавливаем новое состояние для всех дочерних узлов
  for var I := 0 to ParentNode.Count - 1 do
    ParentNode.Item[I].CheckState := NewState;
end;

procedure TMainForm.UpdateParentState(ChildNode: TTreeNode);
var
  ParentNode: TTreeNode;
  AllChecked, AllUnchecked: Boolean;
begin
  ParentNode := ChildNode.Parent;
  if not Assigned(ParentNode) then Exit;

  AllChecked := True;
  AllUnchecked := True;

  // Проверяем состояния всех дочерних узлов
  for var I := 0 to ParentNode.Count - 1 do
  begin
    if ParentNode.Item[I].CheckState = ncsChecked then
      AllUnchecked := False
    else if ParentNode.Item[I].CheckState = ncsUnchecked then
      AllChecked := False
    else
    begin
      AllChecked := False;
      AllUnchecked := False;
    end;

    // Если уже нашли и checked и unchecked - можно прервать проверку
    if not AllChecked and not AllUnchecked then Break;
  end;

  // Устанавливаем соответствующее состояние родителя
  if AllChecked then
    ParentNode.CheckState := ncsChecked
  else if AllUnchecked then
    ParentNode.CheckState := ncsUnchecked
  else
    ParentNode.CheckState := ncsPartial;
end;

Альтернативное решение

Если вам не нравится поведение трех состояний, можно полностью отключить csPartial и имитировать частичное выделение с помощью:

  1. Кастомного рисунка для узлов
  2. Использования изображений вместо стандартных чекбоксов

Пример альтернативного подхода:

procedure TMainForm.TreeView1AdvancedCustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
  var PaintImages, DefaultDraw: Boolean);
var
  CheckRect: TRect;
  DrawState: TThemedButton;
begin
  if Stage = cdPostPaint then
  begin
    if Node.Level = 0 then
    begin
      // Определяем состояние для рисования
      if IsParentPartiallyChecked(Node) then
        DrawState := tbCheckBoxMixedNormal
      else if Node.Checked then
        DrawState := tbCheckBoxCheckedNormal
      else
        DrawState := tbCheckBoxUncheckedNormal;

      // Рисуем кастомный чекбокс
      CheckRect := Node.DisplayRect(True);
      CheckRect.Right := CheckRect.Left - 5;
      CheckRect.Left := CheckRect.Right - 16;
      ThemeServices.DrawElement(TreeView1.Canvas.Handle,
        ThemeServices.GetElementDetails(DrawState), CheckRect);
    end;
  end;
end;

function TMainForm.IsParentPartiallyChecked(ParentNode: TTreeNode): Boolean;
var
  HasChecked, HasUnchecked: Boolean;
begin
  HasChecked := False;
  HasUnchecked := False;

  for var I := 0 to ParentNode.Count - 1 do
  begin
    if ParentNode.Item[I].Checked then
      HasChecked := True
    else
      HasUnchecked := True;

    if HasChecked and HasUnchecked then
      Exit(True);
  end;

  Result := False;
end;

Заключение

Реализация сложного поведения чекбоксов в TreeView требует внимательной обработки событий изменения состояний. Представленное решение позволяет достичь желаемого поведения с частичным выделением родительских узлов, сохраняя при этом привычную пользователю логику взаимодействия. Альтернативный подход с кастомной отрисовкой дает больше гибкости, но требует больше кода для реализации.

Для большинства случаев первого решения будет достаточно, и оно обеспечит стабильную работу с минимальными затратами на реализацию.

Создано по материалам из источника по ссылке.

Реализация TreeView с чекбоксами и частичным выделением в Delphi 12.2, включая автоматическое обновление состояния родительских узлов при изменении дочерних элементов.


Комментарии и вопросы

Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS




Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.


:: Главная :: TTreeView ::


реклама


©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007
Top.Mail.Ru

Время компиляции файла: 2024-12-22 20:14:06
2025-07-17 00:43:17/0.0036270618438721/0