Programowanie w technologii.net wykład 3 Dependency Properties, Routed Events 1/29 Dependency Properties własności zależnościowe - wydajniejsze pamięciowo - dziedziczenie wartości (w drzewie elementów) - powiadomienia o zmianie wartości - potrzebne do stylów, animacji, wiązania danych - używa się ich tak samo, jak zwykłych własności
2/29 klasyczne własności: class FrameworkElement Thickness Margin set... = value; get return...; a jak są definiowane Dependency Properties? najpierw statyczna składowa reprezentująca definiowaną własność: public class FrameworkElement : UIElement,... public static readonly DependencyProperty MarginProperty; //...
3/29 rejestrowanie właściwości w statycznym konstruktorze: static FrameworkElement() FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure); MarginProperty = DependencyProperty.Register("Margin", typeof(thickness), typeof(frameworkelement), metadata, IsMarginValid); //... walidacja (uwaga: to metoda statyczna, zatem sprawdza tylko podaną wartość): private static bool IsMarginValid(object value) Thickness thickness1 = (Thickness)value; if(...) return true; return false;
wrapper (te metody są wołane tylko z kodu C#, a nie XAMLa): public Thickness Margin set SetValue(MarginProperty, value); get return (Thickness)GetValue(MarginProperty); teraz mamy gotową właściwość: myelement.margin = new Thickness(5); jest też dostępne czyszczenie lokalnie ustawionej wartości: myelement.clearvalue(frameworkelement.marginproperty); Property Metadata pozwala na ustawienie kilku dodatkowych cech definiowanej własności najważniejsze: DefaultValue domyślna wartość własności CoerceValueCallback testowanie zgodności wartości PropertyChangedCallback powiadomienie o zmianie wartości ponadto flagi określające wpływ własności na ułożenie lub rozmiar elementu (np. AffectsArrange, AffectsMeasure), na sposób wyświetlania (AffectsRender), zachowania przy wiązaniu danych, triggerach, animacji, interfejsie opartym na stronach (np. IsAnimationProhibited, IsNotDataBindable, Journal) oraz włączające dziedziczenie w drzewie zagnieżdżenia (Inherits) 4/29
5/29 Coercion 1. CoerceValueCallback ma szansę na zmianę dostarczonej wartości albo ją odrzucić 2. ValidateValueCallback sprawdza poprawność wartości (statycznie!) 3. PropertyChangedCallback gdy zmiana zaszła pomyślnie przykład koercji na scrollu i właściwości Maximum: private static object CoerceMaximum(DependencyObject d, object value) RangeBase base1 = (RangeBase)d; if (((double)value) < base1.minimum) return base1.minimum; return value;
podobnie dla Value: internal static object ConstrainToRange(DependencyObject d, object value) double newvalue = (double)value; RangeBase base1 = (RangeBase)d; double minimum = base1.minimum; if (newvalue < minimum) return minimum; double maximum = base1.maximum; if (newvalue > maximum) return maximum; return newvalue; 6/29
w Minimum nie ma koercji, ale jest odpalenie zmiany pozostałych, gdy trzeba: private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) RangeBase base1 = (RangeBase)d; //... base1.coercevalue(rangebase.maximumproperty); base1.coercevalue(rangebase.valueproperty); podobnie Maximum wymusza koercję Value: private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) RangeBase base1 = (RangeBase)d; //... base1.coercevalue(rangebase.valueproperty); Ma to zadbać o właściwe dopasowanie wartości: ScrollBar bar = new ScrollBar(); // Value = 0, Minimum = 0, Maximum = 1 bar.value = 100; // Value = 1 (koercja) bar.minimum = 1; // Value = 1 bar.maximum = 200; // znow odpalona koercja Value i Value = 100 (*) (*) - koercja odpalona z oryginalnie podaną wartością właściwości zatem ustalone jest Value = 100 (!) 7/29
8/29 Shared Dependency Properties Niekiedy kilka klas (w osobnych hierarchiach) korzysta z tej samej własności, np. TextBlock.FontFamily i Control.FontFamily wskazują na tę samą własność zdefiniowaną w klasie TextElement; robi się to wywołując AddOwner: TextBlock.FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock)); Attached Dependency Properties Dotyczą innego elementu niż są zdefiniowane. Np. Grid.Row zdefiniowane jest w Gridzie, a dotyczy elementu w nim osadzonego. Rejestruje się je przy użyciu RegisterAttached: FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata( 0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged)); Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int), typeof(grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative));
9/29 Nie definiuje się dla nich wrappera, gdyż mogą być ustawione dla dowolnego elementu. Zamiast tego korzystamy ze statycznych metod: public static int GetRow(UIElement element) if (element == null) throw new ArgumentNullException(/*...*/); return (int)element.getvalue(grid.rowproperty); public static void SetRow(UIElement element, int value) if (element == null) throw new ArgumentNullException(/*...*/); element.setvalue(grid.rowproperty, value); A tak z tego korzystamy: Grid.SetRow(txtElement, 0); co przekłada się na: txtelement.setvalue(grid.rowproperty, 0);
Jak używane są własności zależnościowe? 10/29
11/29 Gdy zmieni się wartość własności, uruchamiana jest metoda callbackowa PropertyChangedCallback nie odpala ona jednak domyślnie eventów. Zamiast tego powiadamiane są data-bindingi i triggery (będzie o nich mowa w następnych rozdziałach). Jedynie część własności uruchamia jakieś powiązane z ich zmiana zdarzenia (np. TextBox.TextChanged, ScrollBar.ValueChanged). Gdy pobieramy wartość własności zależnościowej, WPF poszukuje jej w: 1. domyślnej wartości 2. wartości odziedziczonej 3. wartości podanej w stylu 4. wartości wpisanej lokalnie (w kodzie lub XAMLu) Tak pobrana wartość, zanim zostanie zwrócona, może być modyfikowana przez wyrażenia, wiązanie danych, dołączone animacje, koercje.
12/29 Zdarzenia w WPF Event Routing Routed Events podróżują po drzewie elementów.
13/29 rodzaje zdarzeń: - direct bezpośrednie (dotyczą tylko jednego elementu) - bubbling (wędrują w górę hierarchii zagnieżdżenia najpierw podnoszone przez element którego dotyczą) - tunneling (wędrują w dół hierarchii zagnieżdżenia najpierw podnoszone przez element najwyższego poziomu okno) przekazany do obsługi argument typu RoutedEventArgs zawiera własność Handled pozwala przerwać tunelowanie/ bąbelkowanie private void DoSomething(object sender, RoutedEventArgs e) if (...) e.handled = true; RoutedEventArgs.Source od kogo pochodzi zdarzenie (przeważnie kontrolka) sender kto je przysłał (gdzie umieszczono obsługę zdarzenia) RoutedEventArgs.RoutedEvent zdarzenie
14/29 Attached Events Wykonawca zdarzenia może być podpięty na poziomie elementu, który podnosi zdarzenie albo do innego elementu powyżej lub poniżej hierarchii zagnieżdżenia: <StackPanel Button.Click="DoSomething" Margin="5"> <Button Name="btn1" Tag="jeden">Przycisk 1</Button> <Button Name="btn2" Tag="dwa">Przycisk 2</Button> <Button Name="btn3" Tag="trzy">Przycisk 3</Button>... </StackPanel> private void DoSomething(object sender, RoutedEventArgs e) object tag = ((FrameworkElement)e.Source).Tag; MessageBox.Show((string)tag); Tunneling Events Tunneling i Bubbling występują w parach (tunneling ma przeważnie przedrostek Preview) najpierw wędruje tunneling, a potem bubbling obsłużenie (ustawienie Handled) dla tunelowego wyłącza bąbelkowe!
15/29 <Label BorderBrush="Black" BorderThickness="1"> <StackPanel> <TextBlock Margin="3">Tekst i ikona</textblock> <Image Source="ikona.jpg" Stretch="None" /> <TextBlock Margin="3">Podpis</TextBlock> </StackPanel> </Label> Label PreviewMouseDown StackPanel PreviewMouseDown Image PreviewMouseDown Image MouseDown StackPanel MouseDown Label MouseDown Label PreviewMouseUp StackPanel PreviewMouseUp Image PreviewMouseUp Image MouseUp StackPanel MouseUp Label MouseUp
Definiowanie zdarzeń w WFP: public abstract class ButtonBase : ContentControl,... // definicja public static readonly RoutedEvent ClickEvent; // rejestracja static ButtonBase() ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent( "Click", RoutingStrategy.Bubble, typeof(routedeventhandler), typeof(buttonbase)); //... // wrapper public event RoutedEventHandler Click add base.addhandler(buttonbase.clickevent, value); remove base.removehandler(buttonbase.clickevent, value); //... ręczne podnoszenie zdarzenia: button.raiseevent( new RoutedEventArgs(ButtonBase.ClickEvent, this)); 16/29
17/29 posługiwanie się zdarzeniami: dołączanie obsługi zdarzenia: <Button Name="btn1" Click="klik">OK</Button> w kodzie: btn1.click += klik; i odłączanie btn1.click -= klik; lub: btn1.addhandler(buttonbase.clickevent, new RoutedEventHandler(klik)); i btn1.removehandler(buttonbase.clickevent, new RoutedEventHandler(klik)); nie powinniśmy dołączać w ten sposób wysokopoziomowych (logicznych) metod, a tylko event handlery - oddelegowujące polecenia do warstwy logiki
18/29 WPF Events Kategorie zdarzeń: czasu życia (gdy element jest ładowany, inicjowany, usuwany) zdarzenia myszy (akcje myszy) zdarzenia klawiatury (akcje klawiatury) zdarzenia stylusa Czasu życia: Podnoszą je wszystkie elementy, gdy są tworzone bądź zwalniane. Initialized gdy utworzono instancję elementu i ustawiono jego właściwości. Inne elementy tego samego okna mogą jeszcze nie istnieć. IsInitialized == true. Jest to zwykłe zdarzenie (nie jest routed). Loaded gdy całe okno zostało zainicjowane, dołączono style i wiązanie danych. Tuż przed jego wyświetleniem. IsLoaded == true. Unloaded gdy element został zwolniony (usunięto go z okna bądź zamknięto okno).
kolejność działań: - tworzona instancja obiektu - przetwarzane i ustawiane właściwości z XAMLa - Initialized (gdy tworzymy okno elementy są inicjowane z dołu do góry te głębiej w zagnieżdżeniu są pierwsze) - ułożenie w kontenerze - Loaded ( z góry do dołu ) - renderowanie (wyświetlenie okna, gdy wszystkie elementy załadowane) Zdarzenia czasu życia dla klasy Window: SourceInitialized ustawiane powiązania do HWND (Win32 API) ContentRendered gdy zawartość okna wyrenderowana po raz pierwszy (okno wyświetlone i gotowe do przyjmowania wejścia) Activated gdy nastąpiło przełączenie do okna (albo załadowane po raz pierwszy) jest to odpowiednik GetFocus kontrolek Deactivated użytkownik przełączył się na inne okno (lub zamknął to) odpowiednik LostFocus Closing okno się zamyka (można to anulować CancelEventArgs.Cancel na true); nie ma Closing, jeśli to system się wyłącza Closed okno zostało zamknięte (ale do jego elementów wciąż mamy jeszcze dostęp) Typowe miejsce dla inicjacji kontrolek Loaded 19/29
20/29 Zdarzenia wejścia: Wszystkie dołączają dwie właściwości: Timestamp (czas zdarzenia w milisekundach) i Device (urządzenie, które odpaliło zdarzenie). Zdarzenia klawiatury: naciśnięcie klawisza: PreviewKeyDown (Tunneling) KeyDown (Bubbling) wpisanie znaku (odpalają je tylko te klawisze, które powodują wpisanie tekstu): PreviewTextInput (Tunneling) TextInput (Bubbling) zwolnienie klawisza: PreviewKeyUp (Tunneling) KeyUp (Bubbling) gdy trzymamy naciśnięty klawisz powtarzane są zarówno oba KeyDown jak i TextInput Uwaga: wiele kontrolek przechwytuje i blokuje te proste zdarzenia (ale nie tunelling) na rzecz własnych (np. TextBox dodaje TextChanged gdy naciśnięcie klawisza spowodowało zmianę tekstu w polu tekstowym).
21/29 Obsługa klawiszy: <Window... KeyDown="KeyEvent" PreviewKeyDown="KeyEvent" KeyUp="KeyEvent" PreviewKeyUp="KeyEvent"> <ScrollViewer Name="scroll"> <Label Name="lblInfo"/> </ScrollViewer> </Window> private void KeyEvent(object sender, KeyEventArgs e) lblinfo.content += "Event: " + e.routedevent + " Key: " + e.key + "\n"; scroll.scrolltobottom(); Uwaga: x to x niezależnie od shift, alt, etc. Ale czym innym jest Key.D0, a czym innymi Key.NumPad0. e.isrepeat pozwala sprawdzić, czy ten event to efekt trzymania klawisza: e.text zwraca tekst, jaki ma otrzymać kontrolka (w zdarzeniach typu TextInput) e.keystates informuje o stanie naciśniętego klawisza
o stanie pozostałych można dowiedzieć się z KeyboardDevice: if ((e.keyboarddevice.modifiers & ModifierKeys.Control) == ModifierKeys.Control) lblinfo.content = "You held the Control key."; metody KeyboardDevice: IsKeyDown() - czy dany klawisz był naciśnięty gdy zaszło zdarzenie IsKeyUp() IsKeyToggled() - tylko dla Caps Lock, Num Lock, Scroll Lock GetKeyStates() - połączenie KeyDown i KeyToggled Keyboard aktualny stan klawiszy: if (Keyboard.IsKeyDown(Key.LeftShift)) lblinfo.content = "The left Shift is held down."; przydatne jak konwertować Key do stringa: KeyConverter converter = new KeyConverter(); string key = converter.converttostring(e.key); 22/29
23/29 PreviewTextInput dobre miejsce do walidacji tekstu w kontrolce (np. gdy chcemy tylko numeryczne ustawiamy Handled gdy nie to co chcemy) private void pnl_previewtextinput(object sender, TextCompositionEventArgs e) short val; if (!Int16.TryParse(e.Text, out val)) // tylko klawisze numeryczne e.handled = true; private void pnl_previewkeydown(object sender, KeyEventArgs e) if (e.key == Key.Space) // spacja tutaj, bo nie podnosi ona PreviewTextInput e.handled = true;
24/29 Mysz MouseEnter kursor wjeżdża nad element MouseLeave opuszcza element nie są routed Poza nimi: PreviewMouseMove (tunneling) i MouseMove (bubbling) gdy mysz się porusza. private void MouseMoved(object sender, MouseEventArgs e) Point pt = e.getposition(this); lblinfo.content = String.Format("Współrzędne: (0,1)", pt.x, pt.y); można sprawdzić też stan przycisków: if(e.leftbutton == MouseButtonState.Pressed)... sprawdzanie położenia wskaźnika myszy metodą statyczną klasy Mouse: Mouse.GetPosition(element)
25/29 Kliknięcia MouseLeftButtonDown MouseLeftButtonUp to samo jest dla Right dla każdego istnieje odpowiednik Preview* (tunelling) MouseWheel i Preview* - obsługa kółka Przekazany parametr MouseButtonEventArgs ma dodatkowo właściwość ClickCount. Niektóre kontrolki przejmują te zdarzenia, a dają dodatkowe (np. Click w Buttonie).
26/29 Przechwytywanie myszy Aby otrzymywać zdarzenia z myszy poza elementem. Mouse.Capture(element) aby zwolnić: Mouse.Capture(null) i jeszcze zdarzenie LostMouseCapture, gdy to stracimy <Window... MouseMove="Canvas_MouseMove" MouseLeftButtonDown="Canvas_MouseLeftButtonDown" MouseLeftButtonUp="Canvas_MouseLeftButtonUp"> <Canvas> <Rectangle Name="box" Width="50" Height="50" Canvas.Top="50" Canvas.Left="100" Fill="Blue" /> </Canvas> </Window>
27/29 private void Canvas_MouseMove(object sender, MouseEventArgs e) if (e.leftbutton == MouseButtonState.Pressed) Point pt = e.getposition(this); box.setvalue(canvas.topproperty, pt.y-25); box.setvalue(canvas.leftproperty, pt.x-25); private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) Mouse.Capture(this); private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) Mouse.Capture(null);
28/29 przydatne do Drag-and-Drop: 1. klikamy i trzymamy przycisk (pewna informacja zapisana i zaczynamy przeciąganie) 2. ruch myszy na inny element, który może przyjąć drop sygnalizacja kursorem 3. zwolnienie przycisku zrzucenie danych <Label MouseDown="lbl1_MouseDown">Źródło</Label> private void lbl1_mousedown(object sender, MouseButtonEventArgs e) Label lbl = (Label)sender; DragDrop.DoDragDrop(lbl, lbl.content, DragDropEffects.Copy); odbiorca wymaga ustawienia: <Label DragEnter="lbl2_DragEnter" Drop="label2_Drop" AllowDrop="True">Cel</Label>
29/29 przyjęcie zrzutu: private void lbl2_drop(object sender, DragEventArgs e) ((Label)sender).Content = e.data.getdata(dataformats.text); sprawdzanie czy dane które możemy przyjąć: private void lbl2_dragenter(object sender, DragEventArgs e) if (e.data.getdatapresent(dataformats.text)) e.effects = DragDropEffects.Copy; else e.effects = DragDropEffects.None;