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.

3 thoughts on “Adorner w WPF na przykładzie zmiany rozmiaru kontrolki”

  1. Ave!

    Chciałem ponarzekać na to że niezbyt zgodny z zasadą DRY twój kod – w szczególności różne TopRightThumbDragi które różnią się tylko znakiem przy działaniu.

    Wygląda to trochę tak (taka prostsza i bezsensowna wersja twojego kodu):

    void Foo()
    {
    Sample += ThumbBottomLeft;
    Sample += ThumbBottomRight;
    Sample += ThumbTopLeft;
    Sample += ThumbTopRight;
    }

    event EventHandler Sample;

    void ThumbBottomLeft(object sender, EventArgs e)
    {
    Math.Max(0, 1234 – 4567);
    Math.Max(0, 1234 + 9605);
    }

    void ThumbBottomRight(object sender, EventArgs e)
    {
    Math.Max(0, 1234 + 4567);
    Math.Max(0, 1234 + 9605);
    }

    void ThumbTopLeft(object sender, EventArgs e)
    {
    Math.Max(0, 1234 – 4567);
    Math.Max(0, 1234 – 9605);
    }

    void ThumbTopRight(object sender, EventArgs e)
    {
    Math.Max(0, 1234 + 4567);
    Math.Max(0, 1234 – 9605);
    }

    Można to powtarzanie usunąć na kilka różnych sposobów, lubiąc programowanie funkcyjne podoba mi się taka opcja:

    void Foo()
    {
    Sample += ThumbGen(false, true);
    Sample += ThumbGen(true, true);
    Sample += ThumbGen(false, false);
    Sample += ThumbGen(true, false);
    }

    event EventHandler Sample;

    EventHandler ThumbGen(bool add1, bool add2)
    {
    return (object sender, EventArgs e) =>
    {
    Math.Max(0, 1234 + (add1 ? 1 : -1) * 4567);
    Math.Max(0, 1234 + (add2 ? 1 : -1) * 9605);
    };
    }

    Pozdrawiam (btw. dobry artykuł, WPF mimo swoich wszechobecnych dziwnostek potrafi pozytywnie zaskoczyć)…

  2. Witam
    Pytanie, ktory kod jest prostrzy i bardziej zrozumialy czyli spelniajacy inna zasade keep it simple:)
    Takie cos:
    ThumbGen(false, true);
    Jest nieladne. Zawiele nie mowi to czytelnikowi.
    Parametry add1 i add2 rowniez nie sa zbyt opisowe imho.

  3. W zasadzie to zastanawiałem się pierwotnie nad tym żeby na przykład zamiast booli zdefiniować lambdy w rodzaju
    top = (x, y) => x – y
    i piasć ThumbGen(top, right), ale w sumie niekoniecznie byłoby to przejrzystsze.

    Tak czy inaczej to kwestia gustu, pewnie mam skrzywienie od pisania w językach funkcyjnych 😉

Leave a Reply

Your email address will not be published.