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:
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:
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.
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ć)…
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.
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 😉