Category Archives: WPF

Code Review: Tworzenie wątków za pomocą Task.Factory.StartNew i ContinueWith

Załóżmy, że nie możemy korzystać z await\async i gdzieś w kodzie mamy następujące wywołania:

public MainWindow()
{
  InitializeComponent();
  Task.Factory.StartNew(LoadData).ContinueWith(UpdateUserInterface, TaskScheduler.FromCurrentSynchronizationContext());
}
private static void UpdateUserInterface(Task obj)
{
  Console.WriteLine("Interfejs zaktualizowany.");
  Task task = Task.Factory.StartNew(AnotherTimeConsumingOperation);
}

private static void AnotherTimeConsumingOperation()
{
  // ???
}

private static void LoadData()
{
  Thread.Sleep(5000);
}

Chcemy wykonać operacje za pomocą Task’ow. LoadData to operacja czasochłonna i wykonujemy ją w wątku z puli. Następnie chcemy zaktualizować interfejs użytkownika więc wywołujemy UpdateUserInterface na wątku głównym UI. Na końcu chcemy wykonać kolejną operację więc standardowo wywołujemy StartNew:

Task.Factory.StartNew(AnotherTimeConsumingOperation);

Niestety, jeśli zajrzyjmy do debuggera, okaże się, że ta metoda jest wykonywana na wątku UI!

image

Dlaczego? W końcu spodziewalibyśmy się, że zostanie ściągnięty wątek z puli, ponieważ nie użyliśmy żadnej flagi mówiącej o synchronizacji (tak jak w poprzednim przykładzie).

Zajrzyjmy do Reflector’a:

[MethodImpl(MethodImplOptions.NoInlining), __DynamicallyInvokable]
public unsafe Task StartNew(Action action)
{
    StackCrawlMark mark;
    Task task;
    mark = 1;
    task = Task.InternalCurrent;
    return Task.InternalStartNew(task, action, null, this.m_defaultCancellationToken, this.GetDefaultScheduler(task), this.m_defaultCreationOptions, 0, &mark);
}

 

Widać, że jest używana metoda GetDefaultScheduler do uzyskania scheduler’a:

private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}
  private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}

 

Łatwo sprawdzić, że defaultscheduler będzie NULL więc pierwszy IF nie zostanie wykonany.

Okazuje się, że w naszym przypadku zostanie użyty aktualny scheduler, którym po wywołaniu UpdateUserInterface będzie SynchronizationContextScheduler. Zatem wszystkie wątki od czasu gdzie przekazaliśmy jako scheduler TaskScheduler.FromCurrentSynchronizationContext() będą wykonywane domyślnie na wątku UI.

Rozwiązanie to oczywiście przekazywanie zawsze schedulera jawnie:

Task.Factory.StartNew(AnotherTimeConsumingOperation,new CancellationToken(),TaskCreationOptions.None,TaskScheduler.Default)

Innymi słowy, jeśli nie przekażemy schedulera jawnie to jest używany TaskScheduler.Current a nie TaskScheduler.Default. Z tego względu, zawsze należy jawnie przekazywać taki parametr ponieważ łatwo popełnić błąd. Myślę, że nie jest to dla większości programistów zbyt intuicyjne. Szczególnie, gdy nie mamy jednego łańcucha wywołań a tworzymy wątki w różnym miejscach.

WPF: Border, CornerRadius oraz przycinanie zawartości

Ostatnio potrzebowałem użyć Border z CornerRadius ustawionym na jakąś wartość, aby móc potem umieścić w środku inne kontrolki (np. obrazek). Moje pierwsze podejście było następujące:

<Border CornerRadius="50" BorderBrush="Black" BorderThickness="2">
  <Image Stretch="Fill" Source="http://i.zdnet.com/blogs/win7-wallpaper-small.png"></Image>
</Border>

Niestety w taki sposób zawartość Border nie zostanie poprawnie przycięta i efekt jest następujący:

image

Znalazłem na forum bardzo ciekawe rozwiązanie z użyciem OpacityMask. Najpierw kod a potem wyjaśnienie:

<Border CornerRadius="50" BorderBrush="Black" BorderThickness="2">
  <Grid>
      <Border CornerRadius="50" Background="White" BorderThickness="2" x:Name="mask"/>
      
      <Image Stretch="Fill" Source="http://i.zdnet.com/blogs/win7-wallpaper-small.png">
              <Image.OpacityMask>
                  <VisualBrush Visual="{Binding ElementName=mask}"></VisualBrush>
              </Image.OpacityMask>
          </Image>
  </Grid>
</Border>

Efekt:

image

Aby zrozumieć powyższy kod należy najpierw wyjaśnić jak działa OpacityMask. OpacityMask to brush, który określa, co na docelowym obrazku będzie wyświetlone. Myślę, że poniższy obrazek z MSDN jest doskonałym wyjaśnieniem:

Object with a LinearGradientBrush opacity mask

Innymi słowy, OpacityMask służy do ustawiania przezroczystości poszczególnych pikseli.

W naszym przypadku korzystamy z VisualBrush, który wygeneruje pędzel na podstawie innej kontrolki. Stworzyliśmy drugą kontrolkę border jako maskę. Posiada ona ten sam CornerRadius, co oznacza, że po wygenerowaniu pędzla, rogi kontrolki będą przezroczyste! I o to właśnie chodzi – po nałożeniu tej maski na obrazek, przytniemy odpowiednio narożniki. To co było przezroczyste w kontrolce “mask” (narożniki) będzie również niewidzialne na docelowym elemencie (obrazku).

Właściwości tylko do odczytu w WPF

Czasami w WPF zachodzi potrzeba stworzenia dependency property ale tylko do odczytu. Zwykłą właściwość bardzo łatwo zaimplementować tzn.:

public double Area
{
  get 
  {
      return _width*_height;
  }
}

Jeśli korzystamy z binding’u danych, wtedy powyższe rozwiązanie jest niewystarczające i należy skorzystać z dependnecy property. Dla przypomnienia, standardowa deklaracja wygląda następująco:

public class MyStateControl : ButtonBase
{
  public MyStateControl() : base() { }
  public Boolean State
  {
    get { return (Boolean)this.GetValue(StateProperty); }
    set { this.SetValue(StateProperty, value); } 
  }
  public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
    "State", typeof(Boolean), typeof(MyStateControl),new PropertyMetadata(false));
}

Ktoś mógłby zaproponować takie rozwiązanie:

public class MyStateControl : ButtonBase
{
  public MyStateControl() : base() { }
  public Boolean State
  {
    get { return (Boolean)this.GetValue(StateProperty); }
  }
  public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
    "State", typeof(Boolean), typeof(MyStateControl),new PropertyMetadata(false));
}

Niestety powyższy kod nie zabroni nikomu zmieniania StateProperty. Powyższe gettery\settery to wyłącznie wrapper C#. WPF czy inni programiści, wciąż mogą używać MyStateControl.StateProperty.SetValue w celu modyfikacji danych.

Prawidłowa implementacja powinna używać DependencyPropertyKey  oraz metodę RegisterReadOnly:

internal static readonly DependencyPropertyKey AquariumSizeKey = DependencyProperty.RegisterReadOnly(
  "AquariumSize",
  typeof(double),
  typeof(Aquarium),
  new PropertyMetadata(double.NaN)
);
public static readonly DependencyProperty AquariumSizeProperty =
  AquariumSizeKey.DependencyProperty;
public double AquariumSize
{
  get { return (double)GetValue(AquariumSizeProperty); }
}

Należy pamiętać aby DependencyPropertyKey było typu internal – dzięki temu, nie będzie można zmodyfikować właściwości z spoza klasy. Publiczny dostęp ma wyłącznie DependencyProperty ale próba modyfikacji zakończy się zawsze wyjątkiem. Jedyny zatem sposób na ustawienie wartości to użycie DependencyPropertyKey, który z kolei ma dostęp internal:

this.SetValue(AquariumSizeKey,size);

Poniższy kod z kolei wywoła wyjątek:

this.SetValue(AquariumSizeProperty,size);

Obiekty Freezable

WPF dostarcza obiekt Freezable. Możemy dziedziczyć po tej klasie aby stworzyć własne obiekty immutable. Ze względów wydajnościowych, można takowy obiekt “zamrozić” i wtedy nie można już go więcej modyfikować – staje się więc immutable.  Przykład:

internal class SampleClass : Freezable
{
   public SampleClass(string text)
   {
       Text = text;
   }

   protected override Freezable CreateInstanceCore()
   {
       return new SampleClass(null);
   }

   public static readonly DependencyProperty TextProperty =
       DependencyProperty.Register("Text", typeof (string), typeof (SampleClass));

   public string Text
   {
       get { return this.GetValue(TextProperty) as string; }
       set { this.SetValue(TextProperty, value); }
   }
}

Przykład nie jest zbyt skomplikowany. Dziedziczymy po Freezable i implementujemy CreateInstanceCore, która będzie służyć do klonowania obiektów ( o tym za chwilę). Aby zamrozić obiekt wystarczy wywołać metodę Freeze:

var sampleClass = new SampleClass("tekst");
sampleClass.Freeze();
sampleClass.Text = "Nie da rady...";

Po zamrożeniu, próba modyfikacji na właściwości Text zakończy się wyjątkiem InvalidOperationException: “Cannot set a property on object ‘WpfApplication2.SampleClass’ because it is in a read-only state.” Freezable sam dba o zabronienie dostępu do właściwości pod warunkiem, że są one typu DependencyProperties. W innym przypadku sprawa wygląda bardziej skomplikowanie. Wiemy, że próba zakończy się wyjątkiem zatem bezpieczniej w kodzie sprawdzać w jakim stanie jest aktualnie obiekt:

if(sampleClass.IsFrozen==false)
    sampleClass.Text = "Value";

Obiekt raz zamrożony nie może zostać odblokowany. Jedyna możliwość to sklonowanie jego np:

var sampleClass = new SampleClass("tekst");
if(sampleClass.CanFreeze)
    sampleClass.Freeze();

var clonedObject = (SampleClass)sampleClass.Clone();
MessageBox.Show(clonedObject.IsFrozen.ToString());// po sklonowaniu, ponownie false
MessageBox.Show(clonedObject.Text);

Clone wykonuje głęboką kopie obiektu. Zaimplementowana prze nas metoda CreateInstanceCore tworzy jedynie instancję obiektu – wszystkie właściwości zostaną sklonowane za nas automatycznie. CreateInstnaceCore ogranicza się zwykle do wywołania domyślnego konstruktora – reszta zostanie wykonana przez Freezable.

Freezable dostarcza również zdarzenie Changed, przydatne podczas monitorowania stanu obiektu:

private void Button_Click(object sender, RoutedEventArgs e)
{
  var sampleClass = new SampleClass("tekst");
  sampleClass.Changed += new EventHandler(SampleClassChanged);
  sampleClass.Text = "Zmiana tekstu";
}
void SampleClassChanged(object sender, EventArgs e)
{
}

Jakie ma to praktyczne zastosowanie? Klasa Freezable została dodana głównie ze względów wydajnościowych. Zamrożony obiekt nie zmieni swojego stanu zatem nie musimy go obserwować. Na przykład kontrolki w WPF muszą być przerenderowane gdy któryś z obiektów zmienił swój stan. Wspomniane wcześniej zdarzenie Changed nie będzie musiało już być monitorowane. Klasy zamrożone są oczywiste bezpieczne z punktu widzenia wielowątkowości ponieważ mamy pewność, że właściwości nie zostaną zmodyfikowane z innego wątku. Dzięki temu, nie trzeba używać czasochłonnych blokad (lock) lub innych metod synchronizacji.

Freezable jest powszechny w kontrolkach WPF. Na przykład zdarzenie Brush.Changed  jest śledzone i w momencie jego wywołania, kontrolka jest przerenderowywana z nowym kolorem. Inne przykłady to klasy Transform , Geometry czy animacje. Wszystkie pędzle systemowe są domyślnie zamrożone. Nie możliwe jest na przykład zmiana koloru dla jakiekolwiek systemowego pędzla:

SolidColorBrush systemBrush = Brushes.White;
MessageBox.Show(systemBrush.IsFrozen.ToString()); // zawsze true

SolidColorBrush customBrush=new SolidColorBrush(Colors.AliceBlue);
MessageBox.Show(customBrush.IsFrozen.ToString()); // domyslnie false

Istnieje również możliwość zamrożenia obiektu prosto z poziomu XAML. Załóżmy, że definiujemy Brush w zasobach:

<LinearGradientBrush ice:Freeze="True" 
xmlns:ice="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" 
/>

WeakEventManager w WPF 4.5

W ostatnich postach pisałem o słabych referencjach oraz podałem przykładowe implementacje obsługi zdarzeń. Niestety mimo generycznego rozwiązania, jednej wady nie udało się usunąć a mianowicie tzw. sacrifice object. Przed rozpoczęciem czytania tego wpisu zachęcam  najpierw do zapoznania się z poprzednimi.

WeakEventManager to klasa WPF. Jak wspomniałem, często nie wiemy kiedy listener jest usuwany z pamięci w różnego rodzaju kontrolkach. Z tego względu to właśnie WPF dostarcza odpowiednią klasę. W wersji 4.5 WeakEventManager znacząco został uproszczony i naprawdę niewiele trzeba pisać własnego kodu. Przed pojawieniem się 4.5 również istniała możliwość wykorzystania WeakEventManager ale wiązało to się  z implementacją interfejsu i pewnej klasy bazowej. Nie było to eleganckie ale do najczęściej wykorzystywanych typów zdarzeń (PropertyChanged, ButtonClick) istniały już zaimplementowane managery. Od wersji 4.5 wszystko jest po prostu generyczne.

Przykład:

public class SampleClass : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   public void RefreshProperty(string propName)
   {
       if (PropertyChanged != null)
           PropertyChanged(this, new PropertyChangedEventArgs(propName));
   }
}
public class ViewModel
{
   public ViewModel(SampleClass sampleClass)
   {
       WeakEventManager<SampleClass, PropertyChangedEventArgs>.AddHandler(sampleClass, "PropertyChanged", SampleClassPropertyChanged);
   }

   void SampleClassPropertyChanged(object sender, PropertyChangedEventArgs e)
   {
       // do something
   }
}

Jak widać wystarczyło wywołać tylko jedną metodę:

WeakEventManager<SampleClass, PropertyChangedEventArgs>.AddHandler(sampleClass, "PropertyChanged", SampleClassPropertyChanged);

AddHandler to statyczna metoda. Pierwszy parametr to obiekt generujący zdarzenie, drugi to nazwa generowanego zdarzenia a trzeci to handler czyli metoda obsługująca te zdarzenie. To jest na prawdę wszystko co musimy napisać! W porównaniu do poprzednich wersji WPF, kod jest dużo prostszy. Dostaliśmy generyczne rozwiązanie, które nigdy nie pozostawia tzw. sacrifice objects (patrz poprzednie wpisy).

Przejdźmy do naszego standardowego testu:

SampleClass sampleClass = new SampleClass();
ViewModel viewModel = new ViewModel(sampleClass);

sampleClass.RefreshProperty(null);
viewModel = null;
GC.Collect();
sampleClass.RefreshProperty(null);

Oczywiście SamplePropertyChanged zostanie wywołane tylko raz.  Aby przekonać się, że bez WeakEventManger mielibyśmy memory leak wystarczy podmienić wywołanie AddHandler, zwykłą silną referencją:

public class ViewModel
{
   public ViewModel(SampleClass sampleClass)
   {
       sampleClass.PropertyChanged += SampleClassPropertyChanged;
   }

   void SampleClassPropertyChanged(object sender, PropertyChangedEventArgs e)
   {
       // do something
   }
}

Po tym zabiegu SampleClassPropertyChanged zostanie wywołane dwukrotnie.

Code review: DispatcherTimer

Dziś miało być o praktycznym przykładzie wykorzystania WeakReference ale stwierdziłem, że najpierw powrócimy znów na chwilę do WPF.  Co myślicie o takiej prostej klasie:

class TimePresenterViewModel:BaseViewModel
{
   private readonly DispatcherTimer _timer;
   const int RefreshTime=6*1000;

   public TimePresenterViewModel()
   {
       _timer=new DispatcherTimer();       
       _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime);
       _timer.Tick += TimerTick;
       _timer.Start();
   }
   void TimerTick(object sender, EventArgs e)
   {
       // jakas logika np.:
       OnPropertyChanged("CurrentTime");
   }
   public string CurrentTime
   {
       get { return DateTime.Now.ToString(); }
   }
}

Następnie gdy użytkownik naciska przycisk, usuwamy obiekt:

public partial class MainWindow : Window
{
   private TimePresenterViewModel _timePresenterViewModel;
   public MainWindow()
   {
       InitializeComponent();
       _timePresenterViewModel=new TimePresenterViewModel();
   }

   private void button_Click(object sender, RoutedEventArgs e)
   {
       _timePresenterViewModel = null;
   }        
}

Wszystko wydaje się OK ale niestety DispatcherTimer spowoduje memory leak. Zajrzyjmy do profiler’a:

image

 

Nie wystarczy usunięcie wszystkich referencji. Łatwo w kodzie o taki błąd ponieważ żadna z naszych referencji nie wskazuje na timer’a a mimo wszystko jest memory leak. Spowodowane jest to wewnętrzną budową timer’a.

Rozwiązaniem jest ręczne zatrzymywanie timer’a przed wyzerowaniem wszystkich referencji.

class TimePresenterViewModel
{
   private DispatcherTimer _timer;
   const int RefreshTime=6*1000;
   
   public void Init()
   {
       if(_timer!=null)
           return;

       _timer = new DispatcherTimer();
       _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime);
       _timer.Tick += TimerTick;
       _timer.Start();
   }
   void TimerTick(object sender, EventArgs e)
   {
         // jakas logika np.:
       OnPropertyChanged("CurrentTime")
   }
   public void Release()
   {
       if(_timer!=null)
       {
           _timer.Stop();
           _timer-=TimerTick;
           _timer=null;
       }
   }
   public string CurrentTime
   {
       get { return DateTime.Now.ToString(); }
   }
}

Sposób użycia:

public partial class MainWindow : Window
{
   private TimePresenterViewModel _timePresenterViewModel;
   public MainWindow()
   {
       InitializeComponent();
       _timePresenterViewModel=new TimePresenterViewModel();
       _timePresenterViewModel.Init();
   }

   private void button_Click(object sender, RoutedEventArgs e)
   {
       _timePresenterViewModel.Release();
       _timePresenterViewModel = null;
   }        
}

Należy zatem pilnować DispatcherTimer’a aby nie okazało się, że tracimy wszystkie referencje a nie wywołaliśmy Stop.

Adorner w WPF na przykładzie zmiany rozmiaru kontrolki

W WPF Adorner to element pozwalający dołączać do innych kontrolek jakieś efekty graficzne. Na przykład jeśli użytkownik wpiszę nieprawidłową wartość w pole edycyjne, wtedy Adorner może dołączyć do takiego pola ikonkę ostrzegając, że wpisano niepoprawną wartość. Adorner zatem doczepiany jest do jakieś kontrolki a rysowany jest na tzw. AdornerLayer.  Spróbujmy zaimplementować adroner, który po doczepieniu do kontrolki pozwala zmienić jej rozmiar. Zacznijmy od zadeklarowania naszego adorner’a:

public class ResizingAdorner:Adorner
{
   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
   }
}

Konstruktor przyjmuje kontrolkę do której doczepiony będzie ResizeAdorner umożliwiający zmianę jej rozmiaru.

Następnie musimy dodać 4 małe kwadraciki, które później zostaną rozmieszczone w narożnikach kontrolki. WPF dostarcza klasę Thumb, która jest kontrolką dostarczającą potrzebne zdarzenia. Dzięki Thumb w łatwy sposób będziemy mogli uchwycić zdarzenie kiedy użytkownik klika na wspomniany kwadracik i przesuwa myszką w celu powiększenia lub pomniejszenia kontrolki. Przykład:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }   
}

Powyższy kod wyłącznie tworzy 4 thumb’y.  VisualCollection oraz metody GetVisualChild, VisualChildrenCount powinny być znane z poprzedniego postu. Musimy je jednak rozmieścić w czterech narożnikach:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
}


ArrangeOverride jest wywoływane gdy nastąpi potrzeba ponownego rozmieszczenia kontrolek – np. gdy rodzic zmieni rozmiar. W ciele metody pozycjonujemy 4 thumb’y do narożników. Gdyby teraz odpalić aplikację i umieścić zaimplementowany adorner na przycisku, to powinniśmy zaobserwować coś takiego:

image

W tej chwili thumb’y nic nie robią – po prostu osiągnęliśmy efekt wizualny. Naszym celem jest jednak umożliwienie zmiany rozmiaru poprzez kliknięcie na jednym z 4 thumbow. Sprawę znacząco uproszcza zdarzenie Thumb.DragDelta:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

       _thumbs[2].DragDelta += BottomLeftThumbDrag;
   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
}

DragDelta jest wywoływane gdy użytkownik klika na thumb i rusza myszką. Przesunięcie, które dokonał jest zapisywane we właściwości VerticalChange oraz HortizontalChange. Analogicznie należy obsłużyć pozostałe 3 thumb’y:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizeAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb();
       _thumbs[1] = CreateThumb();
       _thumbs[2] = CreateThumb();
       _thumbs[3] = CreateThumb();

       _thumbs[0].DragDelta += TopLeftThumbDrag;
       _thumbs[1].DragDelta += TopRightThumbDrag;
       _thumbs[2].DragDelta += BottomLeftThumbDrag;
       _thumbs[3].DragDelta += BottomRightThumbDrag;
   }
   private Thumb CreateThumb()
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
   void BottomRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, args.VerticalChange + adornedElement.Height);
   }
   void TopLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
   void TopRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
}

Kolejne udoskonalenie to zmiana kursora w zależności od tego, który thumb jest pod myszką. Ostateczna wersja gotowa do użycia wygląda następująco:

public class ResizingAdorner:Adorner
{
   private readonly Thumb[] _thumbs;
   private readonly VisualCollection _visualCollection;
   private const float ThumbSize = 10;

   public ResizingAdorner(UIElement adornedElement) : base(adornedElement)
   {
       _visualCollection=new VisualCollection(this);

       _thumbs=new Thumb[4];
       _thumbs[0] = CreateThumb(TopLeftThumbDrag,Cursors.SizeNWSE);
       _thumbs[1] = CreateThumb(TopRightThumbDrag,Cursors.SizeNESW);
       _thumbs[2] = CreateThumb(BottomLeftThumbDrag,Cursors.SizeNESW);
       _thumbs[3] = CreateThumb(BottomRightThumbDrag,Cursors.SizeNWSE);            
   }
   private Thumb CreateThumb(DragDeltaEventHandler dragDeltaEventHandler,Cursor cursor)
   {
       Thumb thumb=new Thumb();
       thumb.Width = thumb.Height = ThumbSize;
       thumb.BorderThickness=new Thickness(1);
       thumb.BorderBrush = Brushes.Black;
       thumb.DragDelta += dragDeltaEventHandler;
       thumb.Cursor = cursor;

       _visualCollection.Add(thumb);

       return thumb;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
   protected override Size ArrangeOverride(Size finalSize)
   {
       double controlWidth = AdornedElement.DesiredSize.Width;
       double controlHeight = AdornedElement.DesiredSize.Height;
       
       _thumbs[0].Arrange(new Rect(-ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[1].Arrange(new Rect(controlWidth - ThumbSize / 2, -ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[2].Arrange(new Rect(-ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));
       _thumbs[3].Arrange(new Rect(controlWidth - ThumbSize / 2, controlHeight - ThumbSize / 2, ThumbSize, ThumbSize));

       return finalSize;
   }
   void BottomLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0,adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0,args.VerticalChange + adornedElement.Height);
   }
   void BottomRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, args.VerticalChange + adornedElement.Height);
   }
   void TopLeftThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width - args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
   void TopRightThumbDrag(object sender, DragDeltaEventArgs args)
   {
       FrameworkElement adornedElement = (FrameworkElement)AdornedElement;

       adornedElement.Width = Math.Max(0, adornedElement.Width + args.HorizontalChange);
       adornedElement.Height = Math.Max(0, adornedElement.Height - args.VerticalChange);
   }
}

Na koniec przyszedł czas na umieszczenie adorner’a na kontrolce. Najpierw zadeklarujmy przycisk wewnątrz AdornerDecorator:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="160" Width="397" xmlns:my="clr-namespace:WpfApplication2">
    <AdornerDecorator>
        <Button Name="button" Width="100" Height="50">Hello World</Button>
    </AdornerDecorator>
</Window>

AdornerDecorator umożliwi nam uzyskanie AdornerLayer, który z kolei będzie służył do rysowania adorner’a. W Code-Behind musimy umieścić zaimplementowany adorner na przycisku:

public MainWindow()
{
  InitializeComponent();
  AdornerLayer layer = AdornerLayer.GetAdornerLayer(button);
  layer.Add(new ResizingAdorner(button));
}    

Efekt końcowy:

image

W dość prosty sposób napisaliśmy adorner’a, który z łatwością i bez żadnych modyfikacji można stosować do wszystkich kontrolek. Implementacja jest kompletnie odizolowana od specyficznej kontrolki.

Klasa VisualCollection

Ostatnio było dużo o wielowątkowości więc może zmieńmy temat i zajmijmy się przez dwa posty tematyką WPF. Celem dzisiejszego wpisu jest wyjaśnienie klasy VisualCollection, która będzie niezbędna aby omówić tzw. Adorner. W większości przypadków, XAML w zupełności wystarcza jeśli chodzi o tworzenie interfejsu użytkownika. W WPF istnieje jednak drugie podejście pozwalające rysować elementy i kontrolki. Można manualnie stworzyć kolekcje VisualCollection oraz dodać do niej wszelkie obiekty. Na przykład:

public class SampleControl:Control
{
   private readonly VisualCollection _visualCollection;
   public SampleControl()
   {
       _visualCollection=new VisualCollection(this);
       _visualCollection.Add(CreateButton());
       _visualCollection.Add(CreateRectangle());
   }
   private DrawingVisual CreateRectangle()
   {
       var drawing=new DrawingVisual();
       using (DrawingContext dc = drawing.RenderOpen())
       {
           dc.DrawRectangle(Brushes.Red,null,new Rect(0,0,50,50));
       }
       return drawing;
   }
   private Button CreateButton()
   {
       var button = new Button();
       button.Content = "Hello world";
       button.Width = button.Height = 300;
       return button;
   }
   protected override Visual GetVisualChild(int index)
   {
       return _visualCollection[index];
   }
   protected override int VisualChildrenCount
   {
       get { return _visualCollection.Count; }
   }
}

VisualCollection to jak sama nazwa mówi, zawiera kolekcję obiektów wizualnych. Mogą być to kontrolki oraz shape’y. Konstruktor kolekcji przyjmuje element stanowiący obiekt nadrzędny (rodzic). Następnie należy dodać wszelkie elementy, które chcemy wyświetlać. Niezbędne jest również przeładowanie dwóch metod: GetVisualChild oraz VisualChildrenCount. Pierwsza z nich zwraca element wizualny pod danym indeksem, druga  z kolei zwraca liczbę obiektów, które kontrolka\element zawiera. Jeśli umieścimy tak utworzoną kontrolkę na formatce wtedy wyświetli się przycisk i kwadrat. Oczywiście to samo moglibyśmy zdefiniować za pomocą XAML lub Controls. VisualCollection to nic innego jak lista obiektów do renderowania. W powyższym przypadku stworzyliśmy ją po prostu manualnie.

Powyższy przykład nie ma w praktyce większego sensu. VisualCollection jest jednak bardzo praktyczny w przypadku implementacji Adorner’a  o czym przekonamy się już w następnym poście.