суббота, 24 декабря 2011 г.

Реализация поведения контролов в Silverlight

Здравствуйте. Сегодня поговорим о том, как определять поведение для различных контролов в Silverlight. Начнем с простого перетаскивания по канвасу. Задача, в принципе, очень простая, и даже просто погуглив можно найти нужное решение (например, раз и два), но я хочу сделать это решение чуть более универсальным.
Итак, в основе перетаскивания лежит использование 3х событий: MouseLeftButtonDown, MouseLeftButtonUp и MouseMove.

Для примера, я возьму код из MSDN.

  1. <UserControl x:Class="DragAndDropSimple.Page"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     Width="400" Height="300">
  5.     <Canvas x:Name="rootCanvas"
  6. Width="640"
  7. Height="480"
  8. Background="Gray"
  9. >
  10.         <!-- You can drag this rectangle around the canvas. -->
  11.         <Rectangle
  12.     MouseLeftButtonDown="Handle_MouseDown"
  13.     MouseMove="Handle_MouseMove"
  14.     MouseLeftButtonUp="Handle_MouseUp"
  15.     Canvas.Left="30" Canvas.Top="30" Fill="Red"
  16.     Width="50" Height="50" />
  17.     </Canvas>
  18.  
  19. </UserControl>

  1. // Global variables used to keep track of the
  2. // mouse position and whether the object is captured
  3. // by the mouse.
  4. bool isMouseCaptured;
  5. double mouseVerticalPosition;
  6. double mouseHorizontalPosition;
  7.  
  8. public void Handle_MouseDown(object sender, MouseEventArgs args)
  9. {
  10.     Rectangle item = sender as Rectangle;
  11.     mouseVerticalPosition = args.GetPosition(null).Y;
  12.     mouseHorizontalPosition = args.GetPosition(null).X;
  13.     isMouseCaptured = true;
  14.     item.CaptureMouse();
  15. }
  16.  
  17. public void Handle_MouseMove(object sender, MouseEventArgs args)
  18. {
  19.     Rectangle item = sender as Rectangle;
  20.     if (isMouseCaptured)
  21.     {
  22.  
  23.         // Calculate the current position of the object.
  24.         double deltaV = args.GetPosition(null).Y - mouseVerticalPosition;
  25.         double deltaH = args.GetPosition(null).X - mouseHorizontalPosition;
  26.         double newTop = deltaV + (double)item.GetValue(Canvas.TopProperty);
  27.         double newLeft = deltaH + (double)item.GetValue(Canvas.LeftProperty);
  28.  
  29.         // Set new position of object.
  30.         item.SetValue(Canvas.TopProperty, newTop);
  31.         item.SetValue(Canvas.LeftProperty, newLeft);
  32.  
  33.         // Update position global variables.
  34.         mouseVerticalPosition = args.GetPosition(null).Y;
  35.         mouseHorizontalPosition = args.GetPosition(null).X;
  36.     }
  37. }
  38.  
  39. public void Handle_MouseUp(object sender, MouseEventArgs args)
  40. {
  41.     Rectangle item = sender as Rectangle;
  42.     isMouseCaptured = false;
  43.     item.ReleaseMouseCapture();
  44.     mouseVerticalPosition = -1;
  45.     mouseHorizontalPosition = -1;
  46. }

Вот и всё стало ясно. Суть в том, чтобы максимально упростить добавление возможности перетаскивания для контрола.
Теперь определимся - перетаскивание, по сути, характеристика контрола, или же его поведение (трактовать можно по разному). Поэтому я для начала решил написать простой интерфейс:

  1. /// <summary>
  2. /// Определяет интерфейс объекта, который представляет собой поведение
  3. /// </summary>
  4. /// <typeparam name="T"></typeparam>
  5. public interface IBehavior<in T> where T:UIElement
  6. {
  7.     void SetTarget(T target);
  8. }

Я выбрал UIElement потому, что с него начинается поддержка нужных нам событий мышки.

  1. namespace System.Windows
  2. {
  3.     public abstract class UIElement : DependencyObject, IAutomationElement
  4.     {
  5.         public static readonly RoutedEvent MouseLeftButtonDownEvent = new RoutedEvent("UIElement.MouseLeftButtonDown");
  6.         public static readonly RoutedEvent MouseLeftButtonUpEvent = new RoutedEvent("UIElement.MouseLeftButtonUp");
  7.         public static readonly RoutedEvent MouseRightButtonDownEvent = new RoutedEvent("UIElement.MouseRightButtonDown");
  8.         public static readonly RoutedEvent MouseRightButtonUpEvent = new RoutedEvent("UIElement.MouseRightButtonUp");
  9.         public static readonly RoutedEvent MouseWheelEvent = new RoutedEvent("UIElement.MouseWheel");
  10.  
  11.         .......

Теперь механизм перетаскивания, как я его вижу:

1. Пользователь нажимает на объект
- запоминаем его Z-индекс, прозрачность и курсор мышки
- Меняем Z-индекс на какой нибудь большой, меняем прозрачность, меняем курсор
- Захватываем мышку

2. Пользователь двигает мышку
- Вычисляем сдвиг мышки и двигаем объект

3. Пользователь отпускает кнопку мышки
- восстанавливаем курсор, прозрачнсть и Z-индекс

Вот класс, реализующий нужный мне функционал


  1. /// <summary>
  2. /// Использую FrameworkElement, так как курсор, z-индекс, прозрачность доступны только с этого класса
  3. /// </summary>
  4. /// <typeparam name="T"></typeparam>
  5. public class DragBehavior<T> : IBehavior<T> where T : FrameworkElement
  6. {
  7.     private T _target;
  8.     private bool _isDrag;
  9.  
  10.     /// <summary>
  11.     /// предыдущее состояние положения курсора и z-индекса
  12.     /// </summary>
  13.     private Point _mousePrevious;
  14.     private int _oldZIndex;
  15.     private double _opacity;
  16.     private Cursor _cursor;
  17.     
  18.  
  19.     /// <summary>
  20.     /// Событие нажатия левой кнопки мыши
  21.     /// </summary>        
  22.     protected virtual void RecMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  23.     {
  24.         if (_target == null) return;
  25.         // не пускать вобытие вверх по дереву
  26.         e.Handled = true;
  27.  
  28.         // сохранение информации о состоянии объекта
  29.         _mousePrevious = e.GetPosition(_target);
  30.         _cursor = _target.Cursor;
  31.         _oldZIndex = Canvas.GetZIndex(_target);
  32.         _opacity = _target.Opacity;
  33.  
  34.  
  35.         _target.CaptureMouse();
  36.  
  37.         _target.Opacity = 0.8;
  38.  
  39.         // Вывод объекта на передний план
  40.         Canvas.SetZIndex(_target, 10000);
  41.  
  42.         // Курсор мыши
  43.         _target.Cursor = Cursors.Hand;
  44.  
  45.         _isDrag = true;
  46.     }
  47.  
  48.     /// <summary>
  49.     /// Отпускание кнопки мыши
  50.     /// </summary>        
  51.     protected virtual void RecMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  52.     {
  53.         if (_target == null) return;
  54.         // не пускать вобытие вверх по дереву
  55.         e.Handled = true;
  56.  
  57.         // Восстановление z-индекса и состояния объекта
  58.         Canvas.SetZIndex(_target, _oldZIndex);
  59.  
  60.         _target.ReleaseMouseCapture();
  61.  
  62.         _target.Opacity = _opacity;
  63.         _target.Cursor = _cursor;
  64.  
  65.         _isDrag = false;
  66.     }
  67.  
  68.     /// <summary>
  69.     /// Событие движения мыши
  70.     /// </summary>        
  71.     protected virtual void RecMouseMove(object sender, MouseEventArgs e)
  72.     {
  73.         if (_target == null) return;
  74.         if (_isDrag)
  75.         {
  76.             // получаем новую позицию объекта (не важно в каких координатах)
  77.             // это нужно только для подсчёта смещения
  78.             var p = e.GetPosition(_target);
  79.  
  80.             // получаем смещение объекта
  81.             var dx = p.X - _mousePrevious.X;
  82.             var dy = p.Y - _mousePrevious.Y;
  83.  
  84.             MoveElement(_target, dx, dy);
  85.         }
  86.     }
  87.  
  88.     private void MoveElement(T target, double dx, double dy)
  89.     {
  90.         var x = Canvas.GetLeft(_target) + dx;
  91.         var y = Canvas.GetTop(_target) + dy;
  92.         Canvas.SetLeft(target, x);
  93.         Canvas.SetTop(target, y);
  94.     }
  95.  
  96.     public void SetTarget(T target)
  97.     {
  98.         if (_target != null)
  99.         {
  100.             _target.MouseLeftButtonDown -= RecMouseLeftButtonDown;
  101.             _target.MouseLeftButtonUp -= RecMouseLeftButtonUp;
  102.             _target.MouseMove -= RecMouseMove;
  103.         }
  104.  
  105.         _target = target;
  106.  
  107.         if (target != null)
  108.         {
  109.             _target.MouseLeftButtonDown += RecMouseLeftButtonDown;
  110.             _target.MouseLeftButtonUp += RecMouseLeftButtonUp;
  111.             _target.MouseMove += RecMouseMove;
  112.         }
  113.     }
  114. }
Пора применить этот класс в деле!
Я разместил на главной и единственной странице проекта один канвас. По нему мы будем двигать объекты.

  1. <UserControl x:Class="SilverlightApplication1.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6.     mc:Ignorable="d"
  7.     d:DesignHeight="500" d:DesignWidth="500">
  8.     <Grid x:Name="LayoutRoot" Background="Gray">
  9.         <Canvas Grid.Column="1" Background="White" Margin="10" x:Name="canvas">            
  10.         </Canvas>
  11.     </Grid>
  12. </UserControl>
В коде я этот канвас заполняю прямоугольниками и придаю им поведение

  1. public partial class MainPage : UserControl
  2. {
  3.     public MainPage()
  4.     {
  5.         InitializeComponent();
  6.         Init();
  7.         InitBehaviors();
  8.     }
  9.  
  10.     /// <summary>
  11.     /// Функция, заполняет канвас случайным образм прямоугольниками разных размеров и цветов
  12.     /// </summary>
  13.     void Init()
  14.     {
  15.         var rand = new Random(DateTime.Now.Millisecond);
  16.  
  17.         for (int i = 0; i < 100; i++)
  18.         {
  19.             var x = rand.NextDouble() * 300;
  20.             var y = rand.NextDouble() * 300;
  21.  
  22.             var a = 255;
  23.             var r = rand.Next(255);
  24.             var g = rand.Next(255);
  25.             var b = rand.Next(255);
  26.  
  27.             var color = new Color {A = (byte)a, R = (byte)r, G = (byte)g, B = (byte)b};
  28.  
  29.             var rect = new Rectangle {Fill = new SolidColorBrush(color), Width = r, Height = b};
  30.             canvas.Children.Add(rect);
  31.  
  32.             Canvas.SetLeft(rect, x);
  33.             Canvas.SetTop(rect, y);
  34.         }
  35.     }
  36.  
  37.     // Список определений поведения, существующий на странице
  38.     readonly IList<IBehavior<FrameworkElement>> _behaviors = new List<IBehavior<FrameworkElement>>();
  39.  
  40.     /// <summary>
  41.     /// Функция, придаёт поведение перетаскивания всем элементам, лежащим на канвасе
  42.     /// </summary>
  43.     void InitBehaviors()
  44.     {
  45.         foreach (UIElement uiElement in canvas.Children)
  46.         {
  47.             if (uiElement is FrameworkElement)
  48.             {
  49.                 var element = uiElement as FrameworkElement;
  50.                 var dragBehavior = new DragBehavior<FrameworkElement>();
  51.                 dragBehavior.SetTarget(element);
  52.                 _behaviors.Add(dragBehavior);
  53.             }
  54.         }
  55.     }
  56. }
После этих работ, получим результат, представленный на картинке ниже.
Теперь весь код, который позволяет перетаскивать фигуры, находится в одном месте и его можно многократно использовать на различных контролах. Но это ещё не всё. Используюя возможности интерфейса IBehavior, можно определять не только перетаскивание, но вообще любое поведение. Для примера, создадим ещё один класс-поведение. Он будет при наведении на контрол увеличивать ему z-индекс, а также плавно увеличивать в размерах. А при потере курсора размер контрола должен плавно прийти в норму. Вот код класса:
  1. /// <summary>
  2. /// Класс при наведении на объект его увеличивает, а при потере курсора - уменьшает
  3. /// </summary>
  4. /// <typeparam name="T"></typeparam>
  5. public class HoverBehavior<T> : IBehavior<T> where T:FrameworkElement
  6. {
  7.     private T _target;
  8.     private ScaleTransform _scaleTransform;
  9.     private int _oldZIndex;
  10.  
  11.     public void SetTarget(T target)
  12.     {
  13.         if (_target != null)
  14.         {
  15.             _target.MouseEnter -= TargetMouseEnter;
  16.             _target.MouseLeave -= TargetMouseLeave;
  17.         }
  18.  
  19.         _target = target;
  20.  
  21.         if (target != null)
  22.         {
  23.             _target.MouseEnter += TargetMouseEnter;
  24.             _target.MouseLeave += TargetMouseLeave;
  25.         }
  26.     }
  27.  
  28.     void TargetMouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
  29.     {
  30.         // Восстановление z-индекса и состояния объекта
  31.         Canvas.SetZIndex(_target, _oldZIndex);
  32.         _scaleTransform = GetScaleTransform(1.1);
  33.         _target.RenderTransform = _scaleTransform;
  34.         SetAnimation(1.1, 1, _scaleTransform);
  35.     }
  36.  
  37.     
  38.     void TargetMouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
  39.     {
  40.         _oldZIndex = Canvas.GetZIndex(_target);
  41.         // Вывод объекта на передний план
  42.         Canvas.SetZIndex(_target, 10000);
  43.         _scaleTransform = GetScaleTransform(1);
  44.         _target.RenderTransform = _scaleTransform;
  45.         SetAnimation(1, 1.1, _scaleTransform);
  46.     }
  47.  
  48.     #region Получение и установвка преобразований и анимаций
  49.  
  50.     private void SetAnimation(double from, double to, Transform transform)
  51.     {
  52.         var doubleAnimationX = GetDoubleAnimation(from, to, transform, "ScaleX");
  53.         var doubleAnimationY = GetDoubleAnimation(from, to, transform, "ScaleY");
  54.  
  55.         var storyBoard = new Storyboard();
  56.         storyBoard.Children.Add(doubleAnimationX);
  57.         storyBoard.Children.Add(doubleAnimationY);
  58.  
  59.         storyBoard.Begin();
  60.     }
  61.  
  62.     private ScaleTransform GetScaleTransform(double scale)
  63.     {
  64.         return new ScaleTransform
  65.         {
  66.             CenterX = _target.Height * 0.5,
  67.             CenterY = _target.Width * 0.5,
  68.             ScaleX = scale,
  69.             ScaleY = scale
  70.         };
  71.     }
  72.  
  73.     private Timeline GetDoubleAnimation(double from, double to, Transform transform, string property)
  74.     {
  75.         var doubleAnimation = new DoubleAnimation
  76.         {
  77.             Duration = new Duration(new TimeSpan(0, 0, 1)),
  78.             From = from,
  79.             To = to
  80.         };
  81.  
  82.         Storyboard.SetTarget(doubleAnimation, transform);
  83.         Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath(property));
  84.  
  85.         return doubleAnimation;
  86.     }
  87.     #endregion
  88. }
Остаётся только изменить одну функцию на странице, добавить в неё создание и привязку только что созданного поведения

  1. /// <summary>
  2. /// Функция, придаёт поведение перетаскивания всем элементам, лежащим на канвасе
  3. /// </summary>
  4. void InitBehaviors()
  5. {
  6.     foreach (UIElement uiElement in canvas.Children)
  7.     {
  8.         if (uiElement is FrameworkElement)
  9.         {
  10.             var element = uiElement as FrameworkElement;
  11.             var dragBehavior = new DragBehavior<FrameworkElement>();
  12.             dragBehavior.SetTarget(element);
  13.             _behaviors.Add(dragBehavior);
  14.             
  15.             var hoverBehavior = new HoverBehavior<FrameworkElement>();
  16.             hoverBehavior.SetTarget(element);
  17.             _behaviors.Add(hoverBehavior);
  18.         }
  19.     }
  20. }
Вы можете скачать исходники. или поглядеть живое демо прямо сейчас:

В результате работы мы, написав сравнительно немного кода, смогли применить его несколько раз на различный контролах. Удобно, не правда ли? Кажется, что интерфейс поведения должен быть частью Silverlight - и непонятно, почему они его не включили. Однако, порыскав в интернете, я всё таки нашел, что искал. Оказывается для реализации поведения можно использовать Behavior Generic Class, но вот найти его можно только в составе Expression Studio. Он находится в сборке System.Windows.Interactivity.dll, которая расположена по адресу (по крайней мере у меня) C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\Silverlight\v4.0\Libraries. Поковыряв программой ILSpy, обнаружим в сборке пару интересных классов. Чтобы не загромождать пост лишним кодом, я покажу только публичные методы классов.

  1. using System;
  2. using System.Globalization;
  3. namespace System.Windows.Interactivity
  4. {
  5.     /// <summary>
  6.     /// Encapsulates state information and zero or more ICommands into an attachable object.
  7.     /// </summary>
  8.     /// <remarks>This is an infrastructure class. Behavior authors should derive from Behavior&lt;T&gt; instead of from this class.</remarks>
  9.     public abstract class Behavior : DependencyObject, IAttachedObject
  10.     {
  11.         /// <summary>
  12.         /// Attaches to the specified object.
  13.         /// </summary>
  14.         /// <param name="dependencyObject">The object to attach to.</param>
  15.         /// <exception cref="T:System.InvalidOperationException">The Behavior is already hosted on a different element.</exception>
  16.         /// <exception cref="T:System.InvalidOperationException">dependencyObject does not satisfy the Behavior type constraint.</exception>
  17.         public void Attach(DependencyObject dependencyObject);
  18.  
  19.         /// <summary>
  20.         /// Detaches this instance from its associated object.
  21.         /// </summary>
  22.         public void Detach();
  23.     }
  24. }

  1. using System;
  2. namespace System.Windows.Interactivity
  3. {
  4.     /// <summary>
  5.     /// Encapsulates state information and zero or more ICommands into an attachable object.
  6.     /// </summary>
  7.     /// <typeparam name="T">The type the <see cref="T:System.Windows.Interactivity.Behavior`1" /> can be attached to.</typeparam>
  8.     /// <remarks>
  9.     ///     Behavior is the base class for providing attachable state and commands to an object.
  10.     ///     The types the Behavior can be attached to can be controlled by the generic parameter.
  11.     ///     Override OnAttached() and OnDetaching() methods to hook and unhook any necessary handlers
  12.     ///     from the AssociatedObject.
  13.     /// </remarks>
  14.     public abstract class Behavior<T> : Behavior where T : DependencyObject
  15.     {
  16.     }
  17. }

После изложенного мной материала выше, назначение показанных методов очевидно.
Это всё. Всем спасибо.

Полезные ссылки
Поведение (Behaviors) контролов в Silverlight 3
Writing Behaviors for Silverlight 3
Top 5 Silverlight Behaviors

Комментариев нет:

Отправить комментарий