При работе с древовидными структурами в Delphi часто возникает необходимость организовать сложную логику взаимодействия с чекбоксами узлов. В этой статье мы рассмотрим, как реализовать TreeView с двухуровневой структурой, где родительские узлы автоматически отмечаются при выборе всех дочерних элементов и отображают частичное выделение, когда выбраны только некоторые дочерние элементы.
Основные требования
Нам необходимо реализовать следующее поведение:
При клике на родительский узел:
Он отмечается, и все его дочерние элементы также отмечаются
При повторном клике - снимается выделение у родителя и всех дочерних элементов
При клике на дочерний узел:
Узел отмечается
Если все дочерние элементы отмечены - родитель отмечается полностью
Если некоторые дочерние элементы отмечены - родитель получает частичное выделение
При повторном клике - узел снимает выделение и обновляет состояние родителя
Основная сложность заключается в том, что при включенном 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 и имитировать частичное выделение с помощью:
Кастомного рисунка для узлов
Использования изображений вместо стандартных чекбоксов
Пример альтернативного подхода:
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
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.