Метод редактирования, именуемый drag-and-drop, является удобным средством редактирования пользовательских представлений данных (будь то текст, графика, деревья, те или иные объектные схемы), поскольку позволяет визуализировать сам процесс изменений. Особенно это касается тех случаев, когда представление данных несколько сложнее, чем просто текст.
В данном конкретном случае поговорим о деревьях. Иерархические зависимости весьма распространены в окружающем нас мире, поэтому задачи сопровождения древовидных структур встают перед многими программистами. В операционной системе Windows имеется стандартный элемент управления, именуемый TreeView, который позволяет работать с деревьями. Его работу прекрасно иллюстрирует дерево дисков/папок Windows Explorer.
В современных системах программирования управление системным TreeView осуществляется через соответствующий объектный класс, поэтому его программирование не вызывает проблем. Это же касается и программирования перестроения дерева посредством drag-and-drop. Универсальная поддержка drag-and-drop в Delphi заложена в базовый класс TControl. Примеры манипулирования имеющимися средствами для практической реализации drag-n-drop есть в хелпе и во многих книгах, посвященных Delphi. Суть состоит в том, что при перемещении объекта A над объектом B при каждом изменении координаты мыши для объекта B вызывается обработчик OnDragOver, который позволяет определить, может ли объект A быть "принят" объектом B в качестве источника drag-and-drop. Если это условие выполняется, и пользователь отпускает кнопку мыши (т.е. совершает "drop"), то для объекта B возникает событие OnDragDrop, в котором и программируется реакция визуального элемента.
Особенность программирования drag-and-drop для визуального элемента TreeView (да и не только для него) состоит в том, что он может быть скроллируемым. В самом деле, пока дерево маленькое и целиком отображается в TreeView, сложностей не возникает. Но как только оно приобретает размеры, выходящие за его рамки по вертикали или горизонтали, а переместить узел дерева нужно как раз в ту ветвь, которая в данный момент не видна, возникает вопрос: как это реализовать. Если исследовать Explorer, то можно заметить, что во время выполнения операции "drag" и подведения перемещаемого объекта к любой из границ элемента TreeView происходит скроллирование его содержимого в соответствующую сторону. Это нам и нужно: реализовать скроллирование содержимого дерева во время перемещения (см. рис.).
В поисках решения было реализовано два варианта. В обоих использовался автоматический старт режима drag-and-drop (DragMode = dmAutomatic).
Вариант первый. Это весьма кодоэкономичный вариант, который сводится к определению в обработчике события TreeView.OnDragOver реакции на нахождение мыши в областях, близких к границам объекта TreeView, и не требует никаких переменных. Код приведен в листинге 1.
Листинг 1.
procedure TTreeDragForm1.TreeViewDragOver (Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin if (Y > 0) and (Y < 30) then SendMessage(TreeView.Handle, WM_VSCROLL, SB_LINEUP, 0); if (Y > TreeView.Height - GetScrollBarHeight - 30) and (Y < TreeView.Height - GetScrollBarHeight) then SendMessage(TreeView.Handle, WM_VSCROLL, SB_LINEDOWN, 0); if (X > 0) and (X < 30) then SendMessage(TreeView.Handle, WM_HSCROLL, SB_LINELEFT, 0); if (X > TreeView.Width - GetScrollBarWidth - 30) and (X < TreeView.Width - GetScrollBarWidth) then SendMessage(TreeView.Handle, WM_HSCROLL, SB_LINERIGHT, 0); end;
При этом предполагается, что размеры областей равны 30 пикселам, а функции GetScrollBarHeight и GetScrollBarWidth возвращают высоту и ширину скроллбаров (они используют вызов API-функции SystemParametersInfo с параметром SPI_GETNONCLIENTMETRICS). Поскольку методов для скроллирования TreeView в оболочке-классе нет, то используется посылка соответствующих сообщений. Координаты мыши передаются в обработчик в качестве параметра.
Вместе с простотой этот вариант реализации имеет и недостатки. Поскольку событие OnDragOver возникает при перемещении мыши, то скроллирование TreeView происходит только при движении мыши в пределах 30-пиксельного прямоугольника вблизи той или иной границы. Если подвигать мышью активно, то это вызовет целую серию событий, и мимо нужного места в дереве можно легко проскочить. Причем, "нагенерировав" событий мыши, придется ждать их отработки, даже если и хочется ее прервать.
Можно, конечно, пытаться совершенствовать рассматриваемый пример введением тех или иных ухищрений, но, как ни покажется это странным, принципиальным недостатком является отсутствие скроллирования при неподвижной мыши. Помимо чисто эргономического неудобства, это связано с еще одной задачей, которая возникает при редактировании дерева посредством drag-and-drop. Обусловлена она самим деревом, которое может быть свернуто или развернуто частично. В этом случае пользователь может начать операцию drag-and-drop при нераскрытом узле-получателе, и его хорошо было бы развернуть во время операции "drag", если пользователь задержал мышь над свернутым элементом дерева. Необходимость реализации задержек наводит на мысль о таймере. В результате получаем другой вариант реализации.
Вариант второй. Он куда увесистее по коду, чем первый, но имеет больше возможностей и допускает некоторые настройки. Скроллирование происходит независимо от событий перемещения при срабатывании таймера (объект класса TTimer, Interval = 100). При этом скроллирование (и разворачивание узла, над которым задержалась мышь) происходит не сразу, а спустя некоторый интервал времени. Для того, чтобы в обработчике таймера знать, что выполняется процесс перемещения, используется булевский флаг (FDragStarted: Boolean), который устанавливается в True в обработчиках дерева OnStartDrag, OnDragOver и в False в обработчиках OnEndDrag, OnDragDrop. Помимо этого флага, используется несколько переменных (размещенных в private-секции формы) и типизованных констант. Ключевой код содержит Листинг 2.
Листинг 2.
type TTreeDragForm2 = class(TForm) …… private FOldMousePos: TPoint; FVScrollDelayCounter: Integer; FHScrollDelayCounter: Integer; FExpandDelayCounter: Integer; FDragStarted: Boolean; …… end; …… const DragScrollAreaWidth: Integer = 40; DragScrollAreaHeight: Integer = 35; DelayCount: Integer = 7; …… procedure TTreeDragForm2.TimerTimer(Sender: TObject); var P: TPoint; Node: TTreeNode; begin Node := nil; if FDragStarted then begin P := TreeView.ScreenToClient(Mouse.CursorPos); // обрабатываем скроллинг по вертикали if (FVScrollDelayCounter > DelayCount) and (P.X > 0) and (P.X < TreeView.Width) then begin if (P.Y > 0) and (P.Y < DragScrollAreaHeight) then begin VScrollControl(TreeView.Handle, SB_LINEUP); // если дергали мышью, то двигаем быстрее if FOldMousePos.X <> P.X then VScrollControl(TreeView.Handle, SB_LINEUP); // если движение идет, то не пытаемся разворачивать узел if Node <> TreeView.GetNodeAt(P.X, P.Y) then FExpandDelayCounter := 0; end; if (P.Y > TreeView.Height - DragScrollAreaHeight - GetScrollBarHeight) and (P.Y < TreeView.Height) then begin VScrollControl(TreeView.Handle, SB_LINEDOWN); // если дергали мышью, то двигаем быстрее if FOldMousePos.X <> P.X then VScrollControl(TreeView.Handle, SB_LINEDOWN); // если движение идет, то не пытаемся разворачивать узел if Node <> TreeView.GetNodeAt(P.X, P.Y) then FExpandDelayCounter := 0; end; end; // не вышли из прямоугольника начала скроллирования по вертикали? if (P.Y > 0) and (P.Y < DragScrollAreaHeight) or (P.Y > TreeView.Height - DragScrollAreaHeight - GetScrollBarHeight) and (P.Y < TreeView.Height) then inc(FVScrollDelayCounter) else FVScrollDelayCounter := 0; // обрабатываем скроллинг по горизонтали …… // не вышли из прямоугольника начала скроллирования по горизонтали? …… // обрабатываем раскрытие свернутого узла if (P.X > 0) and (P.X < TreeView.Width) and (P.Y > 0) and (P.Y < TreeView.Height) and (FExpandDelayCounter > DelayCount) then begin Node := TreeView.GetNodeAt(P.X, P.Y); if (Node <> nil) and Node.HasChildren and not Node.Expanded then Node.Expand(False); end; // проверяем, не сместились ли с узла, над которым задержались if (FOldMousePos.X = P.X) and (FOldMousePos.Y = P.Y) then inc(FExpandDelayCounter) else FExpandDelayCounter := 0; FOldMousePos := P; end; end;
Используются функции GetScrollBarHeight и GetScrollBarWidth, упомянутые в первом варианте реализации. Кроме этого, используются функции VScrollControl и HscrollControl, которые являются просто обертками для функций SendMessage (смотри вариант 1). Размером областей, при попадании в которые мышью начинается скроллинг, можно управлять (константы DragScrollAreaWidth, DragScrollAreaHeight). Задержка скроллирования и развертывания узла определяется значением константы DelayCount. Переменные FVScrollDelayCounter, FHScrollDelayCounter,
FExpandDelayCounter являются счетчиками задержки, соответственно, начала вертикального скроллирования, горизонтального скроллирования, развертывания узла. Поскольку координаты указателя мыши не приходят в обработчик таймера, их приходится выяснять в глобальной переменной Mouse и приводить к координатной системе TreeView. FOldMousePos - переменная для сохранения предыдущего значения координаты указателя мыши.
Если при выполнении операции "drag" подвести мышь к границе TreeView или задержать ее над свернутым узлом, то через некоторое время (определяемое значением DelayCount) дерево будет скроллироваться или узел будет развернут (без рекурсии), соответственно. Если при скроллинге двигать мышью у границы TreeView, то скроллинг будет выполняться в два раза быстрее.
Описание обработки скроллинга по горизонтали в листинге опущена, она аналогична обработке вертикального скроллинга.
Поскольку второй вариант показал приемлемые результаты, то поиск других решений был прекращен. Приведенные реализации нельзя считать идеальными, резервы для оптимизации есть. Если вы знаете принципиально иные решения, поделитесь (если не жалко).
Юрий А. СМАНЦЕР,
georgesman@mail.ru
Горячие темы