LCL Drag Drop/ru

From Free Pascal wiki
Jump to navigationJump to search

English (en) français (fr) русский (ru)


Руководство DoDi по Dragging (перетаскиванию), Dropping (бросанию компонента) и Docking (стыковке)

Элементы управления в графическом интерфейсе можно захватить с помощью мыши, перетащить в другие места и перетащить на другие элементы управления или на рабочий стол. Когда перетаскиваемый элемент управления перемещается в другое место, операция называется "docking". В противном случае исходный и целевой элементы управления могут обмениваться информацией или взаимодействовать любым другим способом, что называется операцией "drag-drop" (тащи-бросай).

Но все это не работает само по себе, приложение должно настроить разрешенные операции, чтобы LCL знал, что делать с каждым элементом управления в графическом интерфейсе. Сложные операции требуют дополнительного кода для управления визуальной обратной связью с пользователем приложения и того, что произойдет, когда перетаскиваемый элемент управления, наконец, будет отброшен.

Для простой поддержки стыковки во всех приложениях вы можете использовать EasyDockingManager или Anchor Docking, что сводит к минимуму необходимые адаптации.

Drag Drop

Это простое и изначально действие только перетаскивания. Участники:

  • Перетаскиваемый элемент управления исходным кодом в графическом интерфейсе
  • Объект перетаскивания, производный от TDragObject
  • Вспомогательные функции
  • События

Теперь давайте посмотрим, как они работают вместе.

Источник перетаскивания

Элемент управления можно сделать перетаскиваемым, установив для его свойства DragMode значение dmAutomatic. Когда пользователь нажимает левую кнопку мыши на таком элементе управления, указатель мыши превращается в индикатор перетаскивания, пока пользователь не отпустит кнопку или не отменит операцию, нажав клавишу Escape. Индикатор теперь следует за движениями мыши и меняет свою форму в соответствии с тем, что произойдет, когда элемент управления будет брошен в текущее место.

Перетаскивание также можно запустить в коде, вызвав SourceControl.BeginDrag. В любом случае вызывается вспомогательная процедура DragManager.DragStart для начала перетаскивания. DragManager - это глобальный экземпляр, реализующий фактическое перетаскивание. Он инициализируется значением по умолчанию TDragManagerDefault и может быть заменен программистом (Вами). В зависимости от DragKind элемента управления запускается операция перетаскивания или стыковки. Исходный элемент управления запускает событие OnStartDrag, в котором программист может предоставить пользовательский объект перетаскивания. Если такой объект не указан, создается объект по умолчанию (TDragControlObject или TDragDockObject). Инициализируются внутренние структуры данных для визуальной обратной связи и прочего.

Перетаскиваемый объект (TDragObject, определенный в элементах управления модуля)

Перетаскиваемый объект получает весь пользовательский ввод при перетаскивании. В реализации по умолчанию он реагирует вызовом либо DragMove, либо DragStop.

Перетаскиваемый объект также отвечает за визуальную обратную связь:

  • GetDragCursor возвращает форму указателя мыши.
  • GetDragImages возвращает список перетаскиваемых изображений, прикрепляемых к указателю мыши.
  • ShowDragImage и HideDragImage реализуют визуальную обратную связь.

Метод EndDrag уведомляет исходный элемент управления об остановке перетаскивания. В случае отмены операции он вызывает SourceControl.DragCanceled, за которым следует SourceControl.DoEndDrag в любом случае, что вызывает событие OnEndDrag.

События

OnStartDrag/Dock

Это событие происходит при запуске операции. Обработчик может предоставить настраиваемый TDragObject или TDragDockObject для операции.

OnDrag/DockOver

Это событие уведомляет возможную цель, когда над ней перетаскивают объект. Обработчик может сигнализировать о принятии или отклонении сброса компонента.

Событие сигнализирует не только о перемещении мыши, но и о том, когда мышь входит в зону целевого элемента управления или покидает ее.

Событие OnDockOver происходит на DockSite, а не в целевом элементе управления под указателем мыши. Обработчик может сигнализировать об отклонении сброса и может регулировать визуальную обратную связь. Событие происходит, когда мышь входит в зону элемента управления или выходит из нее, а также при каждом движении мыши.

OnDrag/DockDrop

Это событие уведомляет целевой элемент управления об окончании операции перетаскивания.

OnEndDrag/Dock

Это событие уведомляет исходный элемент управления об окончании операции перетаскивания.

Основные недостатки

  • Те же недостатки, что и у Delphi VCL.
  • Нет поддержки платформы LCL (кроссплатформа) для перетаскивания компонентов из приложения LCL в любое другое приложение. Чтобы это работало, LCL необходима поддержка протокола XDND в X11, поддержка OLE Drag-n-Drop в Windows и Mac Drag Manager в OSX.
  • Нет поддержки платформы LCL (кроссплатформа) для приема сбрасываемых компонентов внутри приложения LCL из другого приложения (рабочего стола, файлового менеджера и т.д.). Обработчик событий OnDragDrop всегда ожидает, что перетаскиваемый компонент(Source) будет передаваться как потомок TObject, что не будет иметь место при перетаскивании объекта из другого приложения в приложение LCL.

Delphi

Drag Drop

Это простое и изначально действие только перетаскивания. Участники:

  • Позволяющий перетаскивать себя элемент управления в графическом интерфейсе
  • Объект перетаскивания, производный от TDragObject
  • Вспомогательные функции
  • События

Теперь давайте посмотрим, как они работают вместе, на основе исходной реализации Delphi для ясности. Реализация LCL несколько отличается в использовании вспомогательных объектов, подробности будут обсуждены позже.

Перетаскиваемый источник(объект)

Элемент управления можно сделать перетаскиваемым, установив для его свойства DragMode значение dmAutomatic. Когда пользователь нажимает левую кнопку мыши на таком элементе управления, указатель мыши превращается в индикатор перетаскивания, пока пользователь не отпустит кнопку или не отменит операцию, нажав клавишу Escape. Индикатор теперь следует за движениями мыши и меняет свою форму в соответствии с тем, что произойдет, когда элемент управления будет брошен в текущее место.

Перетаскивание также можно запустить программно, вызвав Source.BeginDrag. В любом случае вызывается вспомогательная процедура DragInitControl для начала перетаскивания. Исходный элемент управления вызывает событие StartDrag, в котором приложение может предоставить настраиваемый объект перетаскивания. Если такой объект не указан, создается объект по умолчанию. Инициализируются внутренние структуры данных для визуальной обратной связи и прочего. Затем вызывается DragTo, чтобы показать, что перетаскивание началось.

Перетаскиваемый объект (TDragObject, определенный в элементах управления модуля)

A drag object receives all user input while dragging. In the default implementation it reacts by calling either DragTo or finally DragDone.

The drag object also is responsible for the visual feedback:

  • GetDragCursor returns the mouse pointer shape.
  • GetDragImages returns a list of drag images, to be attached to the mouse pointer.
  • ShowDragImage and HideDragImage implement the visual feedback.

The Finished method notifies the source control when dragging stops. In case of a cancel'd operation it invokes source.DragCanceled, followed by source.DoEndDrag in either case, which raises an EndDrag event.

LCL Modifications

The current LCL implementation has removed input processing from the drag object, so that the application can no more customize the UI. And the LCL implementation is far from being okay :-(

Also the drag object is bypassed in the presentation of the visual feedback.

Helper Functions

Helper functions primarily allow for access to protected properties and methods of the controls and other classes, from outside the Controls unit. They also can perform common tasks, and can encapsulate platform specific and other implementation details.

General Helpers

DragTo (Delphi only, not present in LCL) determines the current drag target, sends notifications to the involved controls, and manages the visual feedback. It also handles the delayed start of a dragging operation, i.e. can wait for the mouse moving away far enough from the starting point.

DragDone (Delphi only, not present in LCL) terminates dragging, either with a drop or cancel of the operation.

CancelDrag (Delphi only, do not confuse with the LCL global procedure CancelDrag) is protected against false calls, otherwise it defers further actions to DragDone.

Special Helpers

Some more public helper procedures exist, which are undocumented in Delphi and are not required for customized dragging operations.

DragFindWindow searches for a drag target window (platform specific).

DragInit initializes the internal management, like mouse capture, and the visual feedback.

DragInitControl creates the drag object, then calls DragInit.

SetCaptureControl handles the mouse capture.

Events

OnStartDrag/Dock

This event occurs when the operation starts. The handler can provide a custom DockObject for the operation.

OnDrag/DockOver

This event notifies a possible target, when an object is dragged over it. The handler can signal to accept or reject an drop.

The event signals not only mouse moves, but also when the mouse enters or leaves a target control.

An OnDockOver event occurs on the DockSite, not on the target control under the mouse. The handler can signal to deny an drop, and can adjust the visual feedback. The event occurs when the mouse enters or leaves a control, and with every single movement of the mouse.

OnDrag/DockDrop

This event notifies the target control of the end of a drag operation.

OnEndDrag/Dock

This event notifies the source control of the end of a drag operation.

Docking specific Events

(this section should be moved to docking topic)

OnGetSiteInfo

This event occurs during the search for a qualifying dock site. Multiple dock sites can overlap each other, reside in overlapping windows, or can be invisible. Every candidate must supply an influence rectangle (catching area), and can signal to reject an drop. The drag manager selects the most appropriate dock site as the target for subsequent OnDockOver events.

OnUnDock

This event occurs when the dragged control has to be removed from its host dock site.

It's unclear what should happen when the handler denies to undock the control.

LCL Implementation

The Delphi implementation of dragging has many flaws, in detail with regards to docking. Until now nobody seems to understand the details of the Delphi dragging model, most code was modeled after the VCL and consequently implements exactly the same misbehaviour. This section, and more important the docking specific page, shall shed some light on the dragging model in general and the severe design and implementation flaws in the Delphi implementation.

The current LCL implementation uses a DragManager object and drag/dock performers instead of helper functions. The reason for this breaking change is unclear - who can explain?

While the Delphi model must be provided for compatibility reasons, the introduction of a DragManager class and object (singleton) allows for the implementation of an alternative drag manager, with improved behaviour, and allows an application to select the most appropriate manager.

DragManager

This object handles the captured input, provides helper methods, and stores dragging and docking parameters.

It's questionable whether the drag object shall not be allowed to handle user input. It makes the system safer, but disallows applications to extend the user interface for specific dragging operations.

The helper methods require different calls, but this is harmless as long as they are only called from LCL components.

More critical is the storage of dragging parameters in the DragManager object, which vanish when a different drag manager is installed. This is fatal in case of the list of registered dock sites :-(

Drag Performers

The internal use of different classes for dragging and docking is okay in so far, as the procedures differ significantly between drag-drop and drag-dock operations. Nonetheless common actions should use common code, e.g. inherited from the common base class.

Message Sequence Chart or UML Sequence Diagram

Provided by Tom Verhoeff

Something along the lines (this is for Delphi; view with monospaced font):

                  Main            Drag          (Optional)        Drag
  User            Event          Source           Drag           Target
 (Mouse)          Loop           Control          Object         Control
    =               =               =                               =
    |               |               |                               |
   1|--MouseDown--->|               |                               |
    |              2#--OnMouseDown->|                               |
    |               |              3#                               |
    |              4#<-BeginDrag()--#                               |
    |               #               #                               |
    |               |               |                               |
    |              5#--OnStartDrag->|                               |
    |               #              6#----Create---->=               |
    |               |               #               |               |
   7|--MouseMove--->|               |               |               |
    |              8#-----------------------------------OnDragOver->|
    |               #               |               |              9#
    |               #               |               |               #
    |               |               |               |               |
  10|---MouseUp---->|               |               |               |
    |             11#-----------------------------------OnDragDrop->|
    |               #               |               |             12#
    |               #               |               |               #
    |               #---OnEndDrag-->|               |               |
    |               |             13#----Destroy--->=               |
    |               |               |                               |


The DragObject is optional. There could be multiple candidate targets. The Main Event Loop determines which event to send to which object, depending on the user action, the cursor location, and the current state of the GUI.

4 somehow triggers 5 (there is a delay depending on the Boolean parameter Immediate to BeginDrag).

6 determines the location of the MouseDown and may use this to decide on what gets dragged (through GetCursorPos, ScreenToClient, MouseToCell). It can call SetDragImage to visualize a thing being dragged.

The chain 7, 8, 9 can occur repeatedly; 9 indicates acceptability of a drop at this target via the Boolean var parameter Accepted.

12 is called only if the immediately preceding OnDragOver of this target returned Accepted = True; i.e. Accepted is a precondition of 12.

13 receives the parameter Target to know where the drop was accepted; if Target = nil, then the drop was cancelled by the user (MouseUp at an unacceptable location), and OnDragDrop was not performed; if Target <> nil, then OnDragDrop was already performed at the Target. The work associated with an actual drop is thus divided over the OnDragDrop (for actions at the target) and OnEndDrag (for actions at the source). Since a Drag Object needs to be destroyed, independent of whether the drop was successful or canceled, it is best to call Free (or Destroy) only in the OnEndDrag handler.

Only one drag can be active at any moment; it either ends in a drop or is canceled.

There are various other things to be included, but this is the fundamental part. (Maybe also see Delphi-Central or "Implementing drag-and-drop in controls".)

I hope this is helpful. Tom

See also