Windows 10 — Современная поддержка «drag-and-drop» для универсальных Windows-приложений
Эта статья основана на общедоступной предварительной версии Windows 10 и Visual Studio 2015.
Продукты и технологии:
Windows 10, XAML, универсальные Windows-приложения
В статье рассматриваются:
- концепции перетаскивания (drag-and-drop);
- реализация источников (drag sources) и мишеней (drop targets) в операции перетаскивания;
- настройка визуальной обратной связи;
- применение асинхронных операций.
Исходный код можно скачать по ссылке msdn.microsoft.com/magazine/
Перетаскивание (операция «drag-and-drop») — интуитивно понятный способ переноса данных в рамках приложения или между приложения на рабочем столе Windows. Дебют этой функциональности состоялся в Windows 3.1 с File Manager, а затем она была распространена на все приложения, поддерживающие OLE 2, например на Microsoft Office. Когда вышла Windows 8, Microsoft ввела новый тип Windows-приложений — приложения Windows Store, которые были рассчитаны на планшеты и всегда отображались в полноэкранном режиме. Поскольку в любой момент на экране было видно только одно приложение, перетаскивание между приложениями потеряло смысл, и были разработаны другие способы обмена данными, такие как Share Charm. Однако в Windows 10 приложения на настольных ПК вновь выполняются в оконном режиме, а значит, на экране отображается несколько окон, и поэтому функциональность перетаскивания вернулась на сцену в виде Universal Windows App API с добавлением новых средств, улучшающих пользовательскую среду (UX).
Концепции перетаскивания
Перетаскивание дает возможность пользователю перемещать данные между приложениями или внутри одного приложения с помощью стандартного жеста («нажать-удерживать-сдвигать» пальцем или «нажать-сдвигать» мышью или пером).
Источник, которым является приложение или область, где инициирован жест перетаскивания, предоставляет данные для перемещения, заполняя объект пакета данных. Этот объект может содержать данные как в стандартных форматах, включая текст, RTF, HTML, битовые карты, элементы хранилища, так и в собственных форматах. Источник также указывает вид поддерживаемых им операций: копирование, перемещение или связывание. Когда указатель освобождается, происходит отпускание объекта (drop). Мишень, которой является приложение или область под указателем, обрабатывает этот пакет данных и возвращает тип выполненной ею операции.
При операции «drag-and-drop» UI перетаскивания обеспечивает визуальное обозначение типа этой операции. Визуальная обратная связь изначально предоставляется источником, но может изменяться мишенями, по которым движется указатель.
Современные операции «drag-and-drop» доступны на настольных компьютерах, планшетах и смартфонах. Они позволяют перемещать данные между любыми видами приложений или внутри них, включая традиционные Windows-приложения, хотя в этой статье основное внимание уделяется XAML API для современных операций «drag-and-drop».
Реализация перетаскивания
Источник и мишень играют разные роли. В приложении могут быть UI-компоненты, которые выступают только в роли источников, только в роли мишеней или и тех, и других одновременно, например как в приложении-примере Photo Booth (рис. 1).
Рис. 1. Источники и мишени
The photomontage is a drag source which can be dropped as an image in Wordpad and other apps | Фотомонтаж — это источник, который можно перетащить как изображение в Wordpad и другие приложения |
Each image becomes a drag source when it contains an image | Каждое изображение становится источником, когда оно содержит некое изображение |
Each image placeholder is a drop target accepting images | Любое место для изображения является мишенью, принимающей изображения |
The photomontage is a drop target accepting text to add a caption | Фотомонтаж является мишенью, которая принимает текст, добавляемый как надпись |
Операция «drag-and-drop» полностью управляется пользовательским вводом, поэтому ее реализация основывается почти всегда на событиях, как показано на рис. 2.
Рис. 2. События «drag-and-drop»
DragStarting event is raised on source | В источнике генерируется событие DragStarting |
DragEnter event is raised on Target #1 | В мишени 1 генерируется событие DragEnter |
DragOver events are raised on Target #1 | В мишени 1 генерируются события DragOver |
DragLeave event is raised on Target #1 | В мишени 1 генерируется событие DragLeave |
DragEnter event is raised on Target #2 | В мишени 2 генерируется событие DragEnter |
DragOver events are raised on Target #2 | В мишени 2 генерируются события DragOver |
Drop event is raised on Target #2 | В мишени 2 генерируется событие Drop |
Source | Источник |
Target #1 | Мишень 1 |
Target #2 | Мишень 2 |
DropCompleted event is raised on source | В источнике генерируется событие DropCompleted |
Left button is pressed | Левая кнопка нажата |
Pointer is moved while button is still pressed | Указатель перемещается при удерживаемой левой кнопке |
Button is released when on a target | Кнопка отпускается, когда указатель попадает на мишень |
Реализация источника В Windows 8.1 элемент управления ListView может быть источником операции «drag-and-drop» внутри приложения, если его свойство CanDragItems установлено в true:
Приложение может обрабатывать событие DragItemsStarting в источнике. Это по-прежнему поддерживается в Windows 10 с добавлением события DragItemsCompleted, которое не требовалось приложениям в Windows 8.1, где мишень и источник должны были относиться к одному и тому же процессу.
Основной источник в современной функциональности «drag-and-drop» — это UIElement, который открывает доступ ко всем средствам современной функциональности «drag-and-drop» и находится в центре внимания этой статьи.
Один из способов сделать UIElement перетаскиваемым — установить его свойство CanDrag в true. Это можно сделать в разметке или в отделенном коде (codebehind). Инфраструктура XAML обрабатывает распознавание жестов и генерирует событие DragStarting, сообщая о начале операции перетаскивания. Приложение должно сконфигурировать DataPackage, заполнив его содержимое и указав, какие операции поддерживаются. Приложение-источник может помещать в DataPackage данные разных форматов, которые сделают его совместимым с большим количеством мишеней (рис. 3). Поддерживаемые операции определяются в типе DataPackageOperation и могут быть Copy, Move, Link или любой их комбинацией.
Рис. 3. Обработка DragStarting и заполнение DataPackage
Вы можете отменять операцию Drag в обработчике событий, установив свойство Cancel параметра DragStartingEventArgs. Так, в нашем приложении-примере в месте для размещения изображений операция Drag начнется, только если на него перетаскивают изображение или файл.
Обработчик DragStartingEvent также является местом, где приложение-источник может настраивать UI перетаскивания, но об этом позже.
В некоторых случаях приложению может потребоваться использовать особый жест для запуска операции «drag-and-drop» или разрешить перетаскивание какого-то элемента управления, который при обычном взаимодействии препятствует использованию стандартного жеста перетаскивания, например TextBox, реагирующий на события, связанные с движением указателя вниз, изменением выделенной в нем области. В таких случаях приложение может реализовать распознавание собственного жеста, а затем инициировать операцию «drag-and-drop» вызовом метода StartDragAsync. Заметьте, что этот метод ожидает передачи идентификатора указателя (pointer identifier) и поэтому вы не можете начать операцию «drag-and-drop» с помощью нестандартных устройств вроде датчика Kinect. После вызова StartDragAsync остальная часть операции «drag-and-drop» следует тому же шаблону, что и при использовании CanDrag=True, включая событие DragStarting.
Как только пользователь освобождает указатель, операция «drag-and-drop» завершается, и источник уведомляется через событие DropCompleted, содержащее DataPackageOperation, возвращаемое мишенью, на которой пользователь отпустил указатель, или DataPackageOperation.None, если указатель был отпущен на мишени, которая не принимает данные, или если была нажата кнопка отмена (Cancel):
StartDragAsync возвращает IAsyncOperation ; приложение-источник может обработать завершение операции либо ожидая на IAsyncOperation, либо обрабатывая событие DropCompleted. Программная отмена после события DragStarting возможна через интерфейс IAsyncOperation, но может озадачить пользователя.
Заметьте: хотя операции «drag-and-drop» как ListView, так и UIElement реализуются одними и теми же системными сервисами и полностью совместимы, они генерируют разные события на стороне источника. То есть, если у ListView свойство CanDragItems установлено в true, генерируются лишь DragItemsStarting и DragItemsCompleted. DragStarting и DropCompleted — события, связанные со свойством CanDrag в UIElement.
Реализация мишени Любой UIElement может быть мишенью при условии, что его свойство AllowDrop установлено в true. В ходе операции «drag-and-drop» в мишени могут генерироваться следующие события: DragEnter, DragOver, DragLeave и Drop. Эти события уже есть в Windows 8.1, но класс DragEventArgs был расширен в Windows 10, чтобы обеспечить приложениям доступ ко всем средствам современной поддержки «drag-and-drop». При обработке события «drag-and-drop» приложение-мишень должно сначала проверить содержимое DataPackage через свойство DataView аргумента события; в большинстве случаев проверка на наличие типа данных достаточна, и это можно делать синхронно. В некоторых случаях, например при передаче файлов, приложению может понадобиться проверка типа доступных файлов до приема или игнорирования DataPackage. Эта операция асинхронная (данный шаблон будет подробно изложен далее в этой статье).
Определив, можно ли обработать эти данные, мишень должна установить свойство AcceptedOperation экземпляра DragEventArgs, чтобы дать возможность системе обеспечить пользователя корректную обратную связь.
Заметьте: если приложение возвращает DataTransferOperation.None из обработчика событий (или операция не принимается источником), перетаскивание будет безрезультатным, даже когда пользователь отпустит указатель прямо над мишенью; вместо этого будет сгенерировано событие DragLeave.
Приложение может обрабатывать либо DragEnter, либо DragOver; AcceptedOperation, возвращаемое DragEnter, сохраняется, если DragOver не обрабатывается. Когда DragEnter вызывается лишь раз, его следует предпочесть в сравнении с DragOver по причинам большей производительности. Однако в случае вложенных мишеней необходимо возвращать корректное значение из DragOver, чтобы родительская мишень могла переопределить его (задание Handled в true предотвращает передачу события вверх к родителю). В приложении-примере в каждом месте для изображений делаются проверки на изображения в DataPackage, и событие направляется родительской сетке, только если изображения нет; это позволяет сетке принимать Text, даже если физически он помещается на место для изображения (рис. 4).
Рис. 4. Обработка DragEnter и проверка DataPackage
Более сложные концепции
Настройка визуальной обратной связи В OLE 2 операция «drag-and-drop» обеспечивала обратную связь лишь изменение курсора мыши согласно ответу мишени на событие DragOver. Современный функционал «drag-and-drop» допускает более сложные сценарии, например более богатую визуальную обратную связь с пользователем. UI перетаскивания состоит из трех частей: визуального контента, глифа и надписи (caption).
Визуальный контент представляет перетаскиваемые данные. Это могут быть перетаскиваемый UIElement (если источником является XAML-приложение), стандартный значок, выбранный системой на основе содержимого DataPackage или пользовательское изображение, заданное приложением.
Глиф отражает тип операции, принятой мишенью. Он может принимать одну из четырех форм в соответствии со значением типа DataPackageOperation. Глиф нельзя настраивать из приложения, но можно скрывать.
Надпись (caption) — это описание, предоставляемое мишенью. В зависимости от приложения-мишени операция Copy может быть, например, добавлением песни в список воспроизведения, загрузкой файла в OneDrive или обычным копированием файла. Надпись дает более точную обратную связь, чем глиф, и играет роль, очень похожую на всплывающую подсказку.
В табл. 1 показано, как источник и мишень могут настраивать эти части. Когда указатель не находится над мишенью, UI перетаскивания выглядит именно так, как его сконфигурировал источник. Когда указатель оказывается над мишенью, некоторые части визуального интерфейса могут быть переопределены мишенью. Как только указатель покидает мишень, все переопределения очищаются.
Табл. 1. Настройки, доступные источнику и мишени
По умолчанию = перетаскиваемый элемент
Может использовать генерируемый системой контент на основе DataPackage
Может использовать любую битовую картуCan use any bitmap
По умолчанию = то, что задано источником
Нельзя использовать контент, генерируемый системой
Может использовать любую битовую карту
Можно показывать или скрыватьCan show or hide
Аспект, основанный на AcceptedOperation
Можно показывать или скрывать
Может использовать любую строку
Можно показывать или скрывать
Когда начинается операция «drag-and-drop» и приложение-источник не пытается настроить UI перетаскивания в обработчике событий DragStarting, XAML делает снимок перетаскиваемого UIElement, который используется в качестве контента UI перетаскивания. Начальный UIElement по-прежнему показывается в своей исходной позиции, чем отличается от поведения ListView, где перетаскиваемые ListViewItem скрываются в начальной позиции. Поскольку снимок перетаскиваемого UIElement создается после генерации события DragStarting, в ходе обработки этого события можно инициировать изменение визуального состояния для изменения снимка. (Заметьте, что состояние UIElement тоже изменяется, и, даже если оно восстанавливается, возможно небольшое мигание.)
При обработке события DragStarting источник может настраивать визуальную обратную связь через свойство DragUI класса DragStartingEventArgs. Например, запрос к системе использовать контент DataPackage для генерации визуального контента осуществляется через SetContentFromDataPackage (рис. 5).
Рис. 5. Использование SetContentFromDataPackage для генерации визуального контента
Вы можете задать собственную битовую карту в качестве контента для UI перетаскивания, используя два разных класса: общеизвестный XAML-класс BitmapImage или новый класс в Windows 10, SoftwareBitmap. Если эта битовая карта является ресурсом приложения, проще использовать BitmapImage и инициализировать его значением URI ресурса:
Если битовую карту нужно генерировать «на лету», когда начинается операция перетаскивания или когда указатель попадает в границы мишени, то SoftwareBitmap можно создать из буфера, генерируемого XAML-классом RenderTargetBitmap, и вы получаете битовую карту с визуальным представлением UIElement, как показано на рис. 6. Этот UIElement должен находиться в визуальном дереве XAML, но не обязан быть видимой частью страницы. Поскольку RenderTargetBitmap выполняет рендеринг асинхронно, здесь нужно указать объект отсрочки (deferral), чтобы XAML знал, что битовая карта по завершении обработчика события может быть не готова, и ожидал заданное время для окончания обновления контента UI перетаскивания. (Механизм отсрочки мы подробно поясним в следующем разделе этой статьи.)
Рис. 6. Настройка контента UI перетаскивания с помощью RenderTargetBitmap и SoftwareBitmap
Конечно, если SoftwareBitmap уже сгенерирован (и он может кешироваться для последующих операций «drag-and-drop»), никакой отсрочки не нужно.
Как для SetContentFromBitmapImage, так и для SetContentFromSoftwareBitmap можно задать точку привязки (anchor point), которая указывает, как позиционировать UI перетаскивания относительно позиции указателя. Если вы используете перегрузку без параметра точки привязки, за указателем будет следовать левый верхний угол вашей битовой карты. Метод GetPosition класса DragStartingEventArgs возвращает позицию указателя относительно какому-либо UIElement, который можно использовать, чтобы задать начальную позицию UI перетаскивания именно там, где расположен перетаскиваемый UIElement.
На стороне мишени различные части перетаскиваемого визуального контента можно настраивать в обработчике либо события DragEnter, либо события DragOver. Настройка осуществляется через свойство DragUIOverride класса DragEventArgs, который предоставляет четыре метода SetContentFrom, идентичные таковым в DragUI на стороне источника, а также четыре свойства, которые позволяют скрывать разные части DragUI и изменять надпись. Наокнец, DragUIOverride также предоставляет метод Clear, сбрасывающий все переопределения DragUI, осуществленные мишенью.
Асинхронные операции Windows Universal Applications API вводит в действие асинхронный шаблон для всех операций, занимающих более нескольких миллисекунд. Это особенно важно в случае перетаскивания, поскольку такие операции полностью управляются пользователем. Ввиду богатства функционала поддержка «drag-and-drop» использует три асинхронных шаблона: асинхронные вызовы, объекты отсрочки (deferrals) и обратные вызовы.
Асинхронные вызовы применяются, когда приложение вызывает системный API, выполнение которого может потребовать некоторого времени. Этот шаблон хорошо известен разработчикам для Windows и реализуется ключевыми словами async и await в C# (или create_task и then в C++). Все методы, извлекающие данные из DataPackage, следуют этому шаблону, например GetBitmapAsync, который используется нашим приложением для получения ссылки на поток изображения (рис. 7).
Рис. 7. Применение асинхронных вызовов для чтения DataPackage
Объекты отсрочки (deferrals) используются, когда инфраструктура XAML выполняет обратный вызов кода приложения, который может сам выдать асинхронный вызов до возврата значения, ожидаемого инфраструктурой. Этот шаблон не был распространен в предыдущих версиях XAML, поэтому давайте остановимся и проанализируем его. Когда кнопка генерирует событие Click, это односторонний вызов в том смысле, что приложению не нужно возвращать никакого значения. Если приложением сделан асинхронный вызов, его результат будет доступен по завершении обработки события Click, но это совершенно нормально, потому что этот обработчик не возвращает значение.
С другой стороны, когда XAML генерирует событие DragEnter или DragOver, он ожидает, что приложение устанавливает свойство AcceptedOperation аргументов события, указывая, можно ли обработать содержимое DataPackage. Если приложение заинтересовано только в доступных типах данных внутри DataPackage, это можно по-прежнему делать синхронно, например:
Однако, если, например, приложение может принимать лишь некоторые типы файлов, оно должно не только проверять типы данных в DataPackage, но и обращаться к данным, что можно делать только асинхронно. Это означает, что выполнение кода приостанавливается, пока данные не будут считаны, и что обработчик события DragEnter (или DragOver) будет выполнен до того, как приложение узнает, может ли оно принять эти данные. Этот сценарий и является целью объекта отсрочки: получая объект отсрочки от объекта DragEventArg, приложение сообщает XAML, что оно отложит некоторую часть своей обработки, а завершая объект отсрочки, приложение уведомляет XAML, что эта обработка закончена и выходные свойства экземпляра DragEventArgs установлены. Вернитесь к рис. 4, чтобы увидеть, как наше приложение-пример проверяет на наличие StorageItem после получения объекта отсрочки.
Объект отсрочки также можно использовать, когда настройка контента UI перетаскивания на стороне мишени требует асинхронных операций, таких как выполнение метода RenderAsync класса RenderTargetBitmap.
На стороне источника операции «drag-and-drop» DragStartingEventArgs тоже предоставляет объект отсрочки, цель которого — обеспечить запуск операции сразу по завершении обработчика события (даже если объект отсрочки не был завершен), чтобы пользователь как можно быстрее получал обратную связь независимо от того, что создание битовой карты для настройки UI перетаскивания занимает некоторое время.
Обратные вызовы применяются в DataPackage, чтобы отложить передачу данных до того момента, когда они реально понадобятся. С помощью этого механизма приложение-источник может оповещать о нескольких форматах в DataPackage, но мишенью считываются только те данные, которые были подготовлены и перемещены. Во многих случаях обратный вызов никогда не происходит, например, если ни одна из мишеней не понимает соответствующий формат данных (что является неплохой оптимизацией производительности).
Заметьте, что во многих случаях предоставление реальных данных потребует асинхронного вызова, а значит, параметр DataProviderRequest этого обратного вызова передает объект отсрочки, чтобы приложения могли уведомлять о том, что им нужно больше времени на обеспечение данных и что эти данные доступны (рис. 8).
Рис. 8. Отложенное предоставление данных
Заключение
При написании приложения, которое манипулирует стандартными форматами данных, например файлами, изображениями или текстом, вы должны подумать о поддержке перетаскивания, так как для пользователей это естественная и хорошо знакомая операция. Основы перетаскивания уже известны тем, кто программировал с применением Windows Forms и Windows Presentation Foundation, что ускоряет освоение этого богатого функционала со своими специфическими концепциями вроде настройки UI перетаскивания и относительно редко применяемых шаблонов, таких как шаблон отсрочки. Если вам нужно поддерживать лишь базовые сценарии перетаскивания, вы можете положиться на свой прежний опыт и создавать простые реализации. Или при желании задействовать все возможности новых средств, чтобы обеспечить адаптированную под конкретных пользователей среду.
Анна Пай (Anna Pai) — инженер ПО в Xbox Team. Ранее работала над Silverlight, Silverlight для Windows Phone, а затем XAML для Windows и Windows Phone.
Ален Цанкетта (Alain Zanchetta) — инженер ПО в Windows XAML Team. Ранее был архитектором в консалтинговом подразделении Microsoft France.
Выражаем благодарность за рецензирование статьи эксперту Microsoft Клементу Фуче (Clément Fauchère).