Свойство Значение Анимация, запускаемая пользовательским DependencyProperty в UserControl?
Вопрос прелюдии:
Как я могу оживить
Angle
СобственностьRotateTransform
изUIElement A
когда значение CustomDependencyProperty
типа логического становитсяTrue
когда я нажимаю наUIElement B
все внутриUserControl
? И в XAML ТОЛЬКО (или в основном)? если возможно:)
Я написал все следующее, чтобы предоставить все необходимые детали моей проблемы. Вы можете прекратить чтение сверху вниз в любое время; даже непосредственно перейти к актуальному вопросу, который находится в первой четверти поста.
Контекст:
Речь идет о триггерах анимации и привязке пользовательских свойств в одном элементе UserControl. Пока не задействовано окно.
Для начала предположим, что я создал UserControl
, который имеет основной Grid
который содержит два других Grids
, Самые простые схемы:
<!-- MyControl.xaml -->
<UserControl ...blahblahblah>
<Grid>
<Grid x:Name="TiltingGrid">
<!-- This Grid contains UIElements that I want to tilt alltogether -->
</Grid>
<Grid>
<Ellipse x:Name="TiltingTrigger" ...blahblahblah>
<!-- This Ellipse is my "click-able" area -->
</Ellipse>
</Grid>
</Grid>
</UserControl>
Затем в коде у меня есть DependencyProperty
называется IsTilting
,
// MyControl.xaml.cs
public bool IsTilting
{
// Default value is : false
get { return (bool)this.GetValue(IsTiltingProperty); }
set { this.SetValue(IsTiltingProperty, value); }
}
private static readonly DependencyProperty IsTiltingProperty =
DependencyProperty.Register(
"IsTilting",
typeof(bool),
typeof(MyControl),
new FrameworkPropertyMetadata(
false,
new PropertyChangedCallback(OnIsTiltingPropertyChanged)));
private static void OnIsTiltingPropertyChanged(...) { ... }
// .. is a classic Callback which calls
// private void OnIsTiltingChanged((bool)e.NewValue)
// and/or
// protected virtual void OnIsTiltingChanged(e) ...
Затем я определил некоторые свойства для моей сетки с именем TiltingGrid
в XAML:
<Grid x:Name="TiltingGrid"
RenderTransformOrigin="0.3, 0.5">
<Grid.RenderTransform>
<RotateTransform
x:Name="TiltRotate" Angle="0.0" />
<!-- Angle is the Property I want to animate... -->
</Grid.RenderTransform>
<!-- This Grid contains UIElements -->
<Path ... />
<Path ... />
<Ellipse ... />
</Grid>
И я хотел бы вызвать наклон при нажатии на определенную область внутри этого UserControl: Ellipse, в secund Grid:
<Grid>
<Ellipse x:Name="TiltingTrigger"
... Fill and Stroke goes here ...
MouseLeftButtonDown="TryTilt_MouseLeftButtonDown"
MouseLeftButtonUp="TryTilt_MouseLeftButtonUp">
</Ellipse>
</Grid>
Если я не ошибаюсь, Ellipse
не имеет события Click, поэтому мне пришлось создать два EventHandlers для MouseLeftButtonDown
а также MouseLeftButtonUp
, Я должен был сделать это таким образом, чтобы иметь возможность:
- Заставьте Ellipse перехватывать Mouse при MouseLeftButtonDown и установите для частного поля значение
true
- Проверьте, находится ли Mouse Point внутри эллипса при MouseLeftButtonUp, установите значение частного поля равным
false
, затем отпустите мышь. - Инвертировать значение свойства DependencyProperty
IsTilting
(true / false), если происходит что-то похожее на "щелчок" (.. который вызовет анимацию наклона, если я смогу разрешить соответствующую привязку..)
Я сохраню вам код MouseLeftDown/Up, но могу предоставить его при необходимости. Что они делают, так это меняют значение DP.
Вопросы):
Я не знаю, как запустить анимацию угла при обновлении моего DependencyProperty. Что ж. Это не актуальная проблема, это недостаток знаний, которые я считаю:
- Я не знаю, как захватить пользовательское событие для использования с
<EventTrigger>
- Я не знаю, как и где вызвать StoryBoard, используя True/False DependencyProperty.
И актуальный вопрос:
Отныне, как мне объявить код, который делает
Angle
СобственностьRotateTransform
оживить от0.0
в45.0
(Рендеринг трансформации моей сетки "TiltingGrid
когда мой ДПIsTilting
установлен вtrue
и оживить обратно0.0
когда этоFalse
?в основном в стиле XAML..?
У меня есть рабочий код в коде C# (подробно ниже). То, что я ищу, - это работоспособное решение в XAML (потому что обычно очень легко переписать почти все в Code Behind, когда вы знаете, как это сделать в XAML)
Что я пробовал до сих пор...
Отныне вам не нужно читать дальше, если вы абсолютно не хотите знать все детали...
1) Запуск анимации с использованием встроенного Ellipse EventTriggers работает только для событий, определенных для этого конкретного UIElement (Enter / Leave / MouseLeftDown...). Выполнено много раз с помощью UIElement.
Но эти триггеры не те, которые мне нужны: моя сетка должна наклоняться в зависимости от On/Off
или же True/False
пользовательское состояние в DP, а не когда происходит что-то подобное мышиному действию.
<Ellipse.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="TiltRotate"
Storyboard.TargetProperty="Angle"
From="0.0" To="45.0"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeave">
...
</Ellipse.Triggers>
Когда мышь входит в эллипс, моя Сетка соответственно наклоняется, но, следовательно, как я могу получить доступ к пользовательским событиям, определенным в моем UserControl?
2) Затем, исходя из приведенной выше схемы, я предположил, что мне просто нужно было создать Routed Event
на моем MyControl
Класс или два, на самом деле:
TiltingActivated
TiltingDisabled
,
public static readonly RoutedEvent TiltingActivatedEvent =
EventManager.RegisterRoutedEvent(
"TiltingActivated",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(EventHandler));
public event RoutedEventHandler TiltingActivated
{
add { AddHandler(MyControl.TiltingActivatedEvent, value); }
remove { RemoveHandler(MyControl.TiltingActivatedEvent, value); }
}
private void RaiseTiltingActivatedEvent()
{
RoutedEventArgs newEventArgs =
new RoutedEventArgs(MyControl.TiltingActivatedEvent, this);
RaiseEvent(newEventArgs);
}
Тогда я звоню RaiseTiltingActivatedEvent()
в одном методе, вызванном моим IsTilting
DependencyProperty Callback, когда его новое значение true
, а также RaiseTiltingDisabledEvent()
когда его новое значение false
,
Замечания: IsTilting
Значение изменяется на true или false при "щелчке" эллипса, и два события запускаются соответственно. Но есть проблема: это не Эллипс, который запускает События, а UserControl
сам.
Во всяком случае, я пытался заменить <EventTrigger RoutedEvent="UIElement.MouseEnter">
со следующими:
Попытка одна:
<EventTrigger RoutedEvent="
{Binding ic:MyControl.TiltingActivated,
ElementName=ThisUserControl}">
.. и я получаю:
"System.Windows.Markup.XamlParseException: (...)"
"A 'Binding' can only be set on a DependencyProperty of a DependencyObject."
Я предполагаю, что не могу привязаться к событию?
Попытка два:
<EventTrigger RoutedEvent="ic:MyControl.TiltingActivated">
.. и я получаю:
"System.NotSupportedException:"
"cannot convert RoutedEventConverter from system.string"
Я предполагаю, что имя RoutedEvent не может быть разрешено? В любом случае, этот подход заставляет меня отходить от моей первоначальной цели: запускать DoubleAnimation при изменении пользовательского свойства (поскольку в более сложных сценариях было бы проще запускать различные анимации и вызывать конкретные методы, все в Code Behind, когда мы можем иметь десятки разных ценностей, чем создание длинных и хитрых вещей на XAML? Лучше всего научиться делать и то и другое. Я очень хочу знать)
3) Затем я наткнулся на эту статью: WPF Animation Tutorial для начинающих.
Код позади создания анимации. Это то, чему я хотел научиться, узнав, как это сделать в XAML. В любом случае, давайте попробуем.
a) Создайте два свойства анимации (личное), одно для наклона анимации, а другое для наклона анимации назад.
private DoubleAnimation p_TiltingPlay = null;
private DoubleAnimation TiltingPlay
{
get {
if (p_TiltingPlay == null) {
p_TiltingPlay =
new DoubleAnimation(
0.0, 45.0, new Duration(TimeSpan.FromSeconds(0.2)));
}
return p_TiltingPlay;
}
}
// Similar thing for TiltingReverse Property...
б) Подпишитесь на два события, а затем установите Angle Animation нашего RotateTransform во время выполнения в коде позади:
private void MyControl_TiltingActivated(object source, EventArgs e)
{
TiltRotate.BeginAnimation(
RotateTransform.AngleProperty, TiltingPlay);
}
// Same thing for MyControl_TiltingDisabled(...)
// Subscribe to the two events in constructor...
public MyControl()
{
InitializeComponent();
this.TiltingActivated +=
new RoutedEventHandler(MyControl_TiltingActivated);
this.TiltingDisabled +=
new RoutedEventHandler(MyControl_TiltingDisabled);
}
В основном, когда я "щелкаю" (MouseButtonLeftDown + Up) на эллипсе:
- Мышь попала в точку
- если в пределах области эллипса, измените DP IsTilting на Not IsTilting.
- Затем IsTilting запускает либо TiltingActivation или TiltingDisabled.
- Оба они захвачены, а затем активируется соответствующая анимация наклона (частные свойства)
<RotateTransform ..>
сетки.
И это работает!!!
Я сказал, что это будет очень просто в коде! (длинный код... да, но это работает) Надеюсь, с шаблонами фрагментов это не так скучно.
Но я до сих пор не знаю, как это сделать в XAML.: /
4) Так как мои пользовательские события, похоже, выходят за рамки XAML, как насчет <Style>
? Обычно связывание в Style
это как дышать. Но, честно говоря, я не знаю, с чего начать.
- целью анимации является
Angle
Собственность<RotateTransform />
применяется кGrid
, - переплетенный Деп. Свойство IsTilting является пользовательским DP
MyControl
неUserControl
, - и один
Ellipse
управляет обновлением DP.
давайте попробуем что-то вроде <RotateTransform.Style>
<RotateTransform ...>
<RotateTransform.st...>
</RotateTransform>
<!-- such thing does not exists -->
или же RotateTransform.Triggers
? ... тоже не существует
ОБНОВЛЕНИЕ: Этот подход работает, объявляя Стиль в Сетка, чтобы оживить, как объяснено в ответе Клеменса. Чтобы разрешить привязку пользовательского свойства UserControl, мне просто нужно было использовать
RelativeSource={RelativeSource AncestorType=UserControl}}
, И чтобы "нацелить" свойство угла в RotateTransform, мне просто нужно было использоватьRenderTransform.Angle
,
Что-то еще?
Я часто вижу образцы, которые устанавливают
DataContext
к чему-то вроде "я". Я не совсем понимаю, что такое DataContext, но я предполагаю, что он по умолчанию указывает все точки разрешения пути на объявленный класс для привязок. Я уже использовал это в одном UserControl, который решил мою проблему, но я не копал глубже, чтобы понять, как и почему. Возможно, это поможет разрешить захват пользовательских событий в коде непосредственно со стороны XAML?Один XAML в основном способ, которым я почти уверен, будет работать:
- создать пользовательский UserControl для этого эллипса, скажем,
EllipseButton
, со своими событиями и свойствами - затем вставьте это в
MyControl
UserControl. - Захватите событие TiltingActivation элемента EllipseButton, чтобы запустить DoubleAnimation в раскадровке EllipseButton, точно так же, как это можно сделать для события Click кнопки.
Это бы хорошо работало, но я нахожу хакерским создание и встраивание другого элемента управления, чтобы иметь возможность доступа к соответствующему пользовательскому событию. MyControl - это не проект SuperWonderfulMegaTop, который потребовал бы такой операции. Я уверен, что упускаю что-то оооочень очевидное; не могу поверить в то, что просто вне мира WPF, в WPF не может быть даже проще.
В любом случае, такие перекрестные соединения сильно подвержены утечкам памяти (возможно, это не так, но я стараюсь избегать этого, когда это возможно...)
- создать пользовательский UserControl для этого эллипса, скажем,
Возможно определяющий
<Grid.Style>
или что-то подобное... но я не знаю как. Я знаю только как пользоваться<Setter>
, Я не знаю, как создать EventTriggers в объявлении стиля. ОБНОВЛЕНИЕ: объяснено ответом Клеменса.Этот SO вопрос (триггер Fire в UserControl на основе DependencyProperty) предлагает создать стиль в
UserControl.Resources
, Попробовал следующее... Это не работает (и там нет анимации в любом случае - я пока не знаю, как объявить анимацию в стиле)
,
<Style TargetType="RotateTransform">
<Style.Triggers>
<DataTrigger
Binding="{Binding IsTilting, ElementName=ThisUserControl}" Value="True">
<Setter Property="Angle" Value="45.0" />
</DataTrigger>
</Style.Triggers>
</Style>
Этот SO вопрос (привязка к углу RotateTransform в DataTemplate не вступает в силу) имеет много неизвестных мне знаний, чтобы быть понятным. Однако, предполагая, что предложенный обходной путь работает, я нигде не вижу ничего похожего на анимацию. Просто привязка к значению, которое не анимировано. Я не думаю, что Угол оживляет себя волшебным образом.
В Code Behind, как рабочий код выше, я мог бы создать еще один DependencyProperty с именем
GridAngle
(двойной), затем привязать свойство угла RotateTransform к этому новому DP, а затем анимировать этот DP напрямую??? Стоит попробовать, но позже: я устал.Я только что обнаружил, что мои зарегистрированные события имеют Bubble Strategy. Это будет иметь значение, если событие будет захвачено некоторыми родительскими контейнерами, но я хочу обрабатывать все непосредственно внутри UserControl, а не как в этом вопросе SO. Однако стратегия туннелирования - которую я пока не понимаю - может сыграть свою роль: позволит ли туннелирование моему эллипсу захватывать события моего UserControl? Приходится читать документацию снова и снова, потому что она все еще очень неясна для меня... Что меня беспокоит, так это то, что я все еще не могу использовать свои пользовательские события в этом UserControl:/
Как насчет CommandBinding? Это кажется очень интересным, но это совершенно другая глава для изучения. Кажется, это связано с большим количеством кода позади, и так как у меня уже есть рабочий код (который выглядит более читабельным для меня...)
В этом вопросе SO (триггеры данных WPF и доски рассказов) принятый ответ, по-видимому, работает, только если я анимирую свойство элемента пользовательского интерфейса, который может иметь
UIElement.Style
определение.RotateTransform
не имеет такой способности.Другой ответ предполагает использование
ContentControl
,ControlTemplate
... Как и в случае с CommandBinding выше, я недостаточно глубоко копал, чтобы понять, как я могу адаптировать это к своему UserControl.Тем не менее, эти ответы, кажется, в основном соответствуют моим потребностям, особенно в способе ContentControl. Позже у меня будет несколько попыток, и я посмотрю, решает ли это XAML главным образом для реализации желаемого поведения.:)
И наконец, этот ТАК вопрос (EventTrigger связывается с событием из DataContext) предлагает использовать
Blend/Interactivity
, Подход выглядит неплохо, но у меня нет Blend SDK, и я не очень хочу этого делать, если мне абсолютно не нужно... Снова: еще одна целая глава, чтобы поесть...
Примечание:
Как вы уже догадались, я новичок в WPF/XAML (я знаю, что это не оправдание), который я начал изучать несколько недель назад. Я вроде "все это было бы очень легко сделать в WinForms прямо сейчас...", но, возможно, вы могли бы помочь мне понять, насколько легко было бы добиться этого в WPF:)
Я много искал (знаю, что это тоже не оправдание), но мне не повезло на этот раз. - Ладно, я только что прочитал три десятка статей, проектов кода и тем SO, а также документацию MSDN о триггерах, анимациях, перенаправленных событиях... кажется, просто полирует поверхность, не углубляясь в ядро (кажется, что MS думает, что наследуется от Баттона есть способ решить практически все...)
1 ответ
Длинный вопрос, короткий ответ. Используйте визуальные состояния:
<UserControl ...>
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="TiltedState">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="TiltingGrid"
Storyboard.TargetProperty="RenderTransform.Angle"
To="45" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
<Grid.RenderTransform>
<RotateTransform/>
</Grid.RenderTransform>
...
</Grid>
</Grid>
</UserControl>
Всякий раз, когда выполняется соответствующее условие, звоните
VisualStateManager.GoToState(this, "TiltedState", true);
в коде UserControl позади. Это, конечно, также может быть вызвано в PropertyChangedCallback свойства зависимостей.
Без использования визуальных состояний вы можете создать стиль для вашей TiltingGrid, который использует DataTrigger с привязкой к вашему UserControl's IsTilted
имущество:
<Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding IsTilted,
RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.Angle"
To="45" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.Angle"
To="0" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RenderTransform>
<RotateTransform/>
</Grid.RenderTransform>
...
</Grid>