В WPF нормальное срабатывание команды кнопки происходит немедленно; но иногда нам нужно предотвратить ложное срабатывание при непреднамеренном нажатии; Итак, как нажать и удерживать кнопку в течение некоторого времени, например 1 с или 500 мс, а затем вызвать команду привязки.
Я надеюсь, что это будет работать как поведение, просто в Xaml для настройки времени действия кнопки.
Большое спасибо, BionicCode! Вдохновленный вами, я немного меняю, и все работает хорошо:
public class DelayButton : Button
{
/// <summary>
/// Delay Milliseconds
/// </summary>
public int DelayInMilliseconds
{
get => (int)GetValue(DelayInMillisecondsProperty);
set => SetValue(DelayInMillisecondsProperty, value);
}
public static readonly DependencyProperty DelayInMillisecondsProperty = DependencyProperty.Register(
nameof(DelayInMilliseconds),
typeof(int),
typeof(DelayButton),
new PropertyMetadata(0));
/// <summary>
/// Delay Command
/// </summary>
public ICommand DelayCommand
{
get { return (ICommand)GetValue(DelayCommandProperty); }
set { SetValue(DelayCommandProperty, value); }
}
public static readonly DependencyProperty DelayCommandProperty = DependencyProperty.Register("DelayCommand",
typeof(ICommand),
typeof(DelayButton),
new PropertyMetadata(null));
/// <summary>
/// cts
/// </summary>
private CancellationTokenSource? delayCancellationTokenSource;
/// <summary>
/// Trigger flag
/// </summary>
private bool isClickValid = false;
/// <summary>
/// Mouse Down Event
/// </summary>
/// <param name = "e"></param>
protected override async void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (DelayInMilliseconds <= 0)
{
//One press,one trigger
this.isClickValid = true;
//Execute DelayCommand
DelayCommand?.Execute(this);
return;
}
try
{
this.delayCancellationTokenSource = new CancellationTokenSource();
await Task.Delay(TimeSpan.FromMilliseconds(this.DelayInMilliseconds), this.delayCancellationTokenSource.Token);
//cheack mouse and not triggr
if (e.LeftButton is MouseButtonState.Pressed && !this.isClickValid)
{
//One press,one trigger
this.isClickValid = true;
//Execute DelayCommand
DelayCommand?.Execute(this);
}
}
catch (OperationCanceledException)
{
return;
}
finally
{
this.delayCancellationTokenSource?.Dispose();
this.delayCancellationTokenSource = null;
}
}
/// <summary>
/// Mouse Up Event
/// </summary>
/// <param name = "e"></param>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (!this.isClickValid)
{
//time not enough
this.delayCancellationTokenSource?.Cancel();
}
else
{
//time enough,For Next
this.isClickValid = false;
}
}
/// <summary>
/// Mouse Leave Event
/// </summary>
/// <param name = "e"></param>
protected override void OnMouseLeave(MouseEventArgs e)
{
this.delayCancellationTokenSource?.Cancel();
}
}
Вы можете использовать Task.Delay
и отмену задачи, чтобы отложить действия кнопки.
Чтобы фактически отменить дребезг кнопки (интерпретировать несколько щелчков в течение определенного периода времени как один щелчок), вам придется удалить часть отмены и защитить обработчики событий от повторного входа (или использовать таймер, который можно запустить только один раз в течение периода устранения дребезга). Но судя по вашему вопросу, вам просто нужна простая отсрочка.
С точки зрения UX задержка нажатия кнопки значительно ухудшает пользовательский опыт. Это не то поведение, которое пользователь ожидает от кнопки. И не интуитивно понятно, что вам нужно удерживать кнопку нажатой в течение неизвестного времени, чтобы вызвать действие. Я бы посчитал это запахом дизайна пользовательского интерфейса.
Если кнопка будет вызывать важные действия, вам следует рассмотреть возможность отображения диалогового окна подтверждения («Вы уверены?» — «ОК» или «Отмена»). Тогда отмена этого диалогового окна может восстановить исходные значения (или просто проглотить действие кнопки).
Я думаю, что это гораздо лучше (с точки зрения UX), чем использование кнопки с задержкой.
Кнопки — «нажми и забудь». Существуют хорошо известные исключения, когда удержание кнопки (например, RepeatButton
) будет постоянно увеличивать значение, например. значение даты. В любом другом сценарии кнопка должна выполняться при нажатии, где продолжительность нажатого состояния не имеет значения.
В следующем примере создается пользовательский DelayButton
, который поддерживает все режимы щелчка (нажатие, отпускание, наведение), а также ввод с клавиатуры (пробел и ввод), и который задерживает событие Button.Click
и вызов Button.Command
.
Он также показывает визуальную обратную связь, чтобы дать пользователю понять, как долго ему нужно нажимать кнопку (полоса «заряжается», чтобы заполнить кнопку, и меняет цвет с красного на зеленый, когда задержка истечет и действие начнется:
<!-- Use DelayInMilliseconds = "0" to use it as a normal button -->
<DelayButton DelayInMilliseconds = "1000" />
public class DelayButton : Button
{
public int DelayInMilliseconds
{
get => (int)GetValue(DelayInMillisecondsProperty);
set => SetValue(DelayInMillisecondsProperty, value);
}
public static readonly DependencyProperty DelayInMillisecondsProperty = DependencyProperty.Register(
nameof(DelayInMilliseconds),
typeof(int),
typeof(DelayButton),
new PropertyMetadata(0, OnDelayInMillisecondsChanged));
private static void OnDelayInMillisecondsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((DelayButton)d).UpdateProgressAnimation();
public SolidColorBrush ProgressBrush
{
get => (SolidColorBrush)GetValue(ProgressBrushProperty);
set => SetValue(ProgressBrushProperty, value);
}
public static readonly DependencyProperty ProgressBrushProperty = DependencyProperty.Register(
nameof(ProgressBrush),
typeof(SolidColorBrush),
typeof(DelayButton),
new PropertyMetadata(Brushes.DarkRed, OnProgressBrushChanged));
private static void OnProgressBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((DelayButton)d).UpdateProgressAnimation();
private CancellationTokenSource? delayCancellationTokenSource;
private bool isClickValid;
private bool isExecutingKeyAction;
private bool isExecutingMouseAction;
private int reentrancyCounter;
private ProgressBar part_ProgressBar;
private Storyboard progressStoryBoard;
static DelayButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DelayButton), new FrameworkPropertyMetadata(typeof(DelayButton)));
}
public DelayButton()
{
this.ClickMode = ClickMode.Press;
this.progressStoryBoard = new Storyboard()
{
FillBehavior = FillBehavior.HoldEnd,
};
UpdateProgressAnimation();
}
private void UpdateProgressAnimation()
{
if (this.progressStoryBoard.IsFrozen)
{
this.progressStoryBoard = this.progressStoryBoard.Clone();
}
var delayDuration = TimeSpan.FromMilliseconds(this.DelayInMilliseconds);
var progressAnimation = new DoubleAnimation(0, 100, new Duration(delayDuration), FillBehavior.HoldEnd);
Storyboard.SetTargetName(progressAnimation, "PART_ProgressBar");
Storyboard.SetTargetProperty(progressAnimation, new PropertyPath(ProgressBar.ValueProperty));
this.progressStoryBoard.Children.Add(progressAnimation);
var colorAnimation = new ColorAnimation(this.ProgressBrush.Color, Colors.Green, new Duration(delayDuration), FillBehavior.HoldEnd);
Storyboard.SetTargetName(colorAnimation, "PART_ProgressBar");
Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("(0).(1)", Control.ForegroundProperty, SolidColorBrush.ColorProperty));
this.progressStoryBoard.Children.Add(colorAnimation);
this.progressStoryBoard.Freeze();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.part_ProgressBar = GetTemplateChild("PART_ProgressBar") as ProgressBar;
}
protected override async void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (this.isExecutingKeyAction || this.ClickMode is ClickMode.Hover)
{
return;
}
this.isExecutingMouseAction = true;
_ = Focus();
_ = Mouse.Capture(this);
try
{
this.delayCancellationTokenSource = new CancellationTokenSource();
await DelayActionAsync(this.delayCancellationTokenSource.Token);
base.OnMouseLeftButtonDown(e);
}
catch (OperationCanceledException)
{
return;
}
finally
{
this.delayCancellationTokenSource?.Dispose();
this.delayCancellationTokenSource = null;
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (this.isExecutingKeyAction || this.ClickMode is ClickMode.Hover)
{
return;
}
this.delayCancellationTokenSource?.Cancel();
StopProgressAnimation();
if (this.ClickMode is ClickMode.Release)
{
if (!this.isClickValid)
{
_ = Mouse.Capture(null);
this.isExecutingMouseAction = false;
return;
}
}
base.OnMouseLeftButtonUp(e);
this.isClickValid = false;
this.isExecutingMouseAction = false;
}
protected override async void OnMouseEnter(MouseEventArgs e)
{
if (this.isExecutingKeyAction)
{
return;
}
if (this.ClickMode is ClickMode.Hover)
{
try
{
this.isExecutingMouseAction = true;
this.delayCancellationTokenSource = new CancellationTokenSource();
await DelayActionAsync(this.delayCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
this.delayCancellationTokenSource?.Dispose();
this.delayCancellationTokenSource = null;
}
}
base.OnMouseEnter(e);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
if (this.isExecutingKeyAction)
{
return;
}
if (this.ClickMode is ClickMode.Hover)
{
this.delayCancellationTokenSource?.Cancel();
StopProgressAnimation();
this.isExecutingMouseAction = false;
}
base.OnMouseLeave(e);
}
protected override async void OnKeyDown(KeyEventArgs e)
{
if (this.ClickMode is ClickMode.Hover)
{
return;
}
if (e.Key is Key.Enter or Key.Space)
{
if (this.isExecutingMouseAction || this.reentrancyCounter > 0)
{
return;
}
try
{
this.reentrancyCounter++;
this.isExecutingKeyAction = true;
this.delayCancellationTokenSource = new CancellationTokenSource();
await DelayActionAsync(this.delayCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
this.delayCancellationTokenSource?.Dispose();
this.delayCancellationTokenSource = null;
}
}
base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (this.ClickMode is ClickMode.Hover)
{
return;
}
if (e.Key is Key.Enter or Key.Space)
{
if (this.isExecutingMouseAction)
{
return;
}
this.delayCancellationTokenSource?.Cancel();
StopProgressAnimation();
this.reentrancyCounter--;
if (this.ClickMode is ClickMode.Release)
{
if (!this.isClickValid)
{
this.isExecutingKeyAction = false;
return;
}
}
}
base.OnKeyUp(e);
this.isClickValid = false;
this.isExecutingKeyAction = false;
}
private async Task DelayActionAsync(CancellationToken cancellationToken)
{
var delayDuration = TimeSpan.FromMilliseconds(this.DelayInMilliseconds);
cancellationToken.ThrowIfCancellationRequested();
StartProgressAnimation();
await Task.Delay(delayDuration, cancellationToken);
this.isClickValid = true;
}
private void StartProgressAnimation()
{
if (this.part_ProgressBar is null)
{
return;
}
this.progressStoryBoard.Begin(this.part_ProgressBar, isControllable: true);
}
private void StopProgressAnimation()
{
if (this.part_ProgressBar is null)
{
return;
}
this.progressStoryBoard.Stop(this.part_ProgressBar);
this.progressStoryBoard.Remove(this.part_ProgressBar);
}
}
Общий.xaml
<ResourceDictionary xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local = "clr-namespace:WpfApp1">
<Style x:Key = "FocusVisual">
<Setter Property = "Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin = "2"
StrokeDashArray = "1 2"
Stroke = "{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
SnapsToDevicePixels = "true"
StrokeThickness = "1" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key = "Button.Static.Background"
Color = "#FFDDDDDD" />
<SolidColorBrush x:Key = "Button.Static.Border"
Color = "#FF707070" />
<SolidColorBrush x:Key = "Button.MouseOver.Background"
Color = "#FFBEE6FD" />
<SolidColorBrush x:Key = "Button.MouseOver.Border"
Color = "#FF3C7FB1" />
<SolidColorBrush x:Key = "Button.Pressed.Background"
Color = "#FFC4E5F6" />
<SolidColorBrush x:Key = "Button.Pressed.Border"
Color = "#FF2C628B" />
<SolidColorBrush x:Key = "Button.Disabled.Background"
Color = "#FFF4F4F4" />
<SolidColorBrush x:Key = "Button.Disabled.Border"
Color = "#FFADB2B5" />
<SolidColorBrush x:Key = "Button.Disabled.Foreground"
Color = "#FF838383" />
<Style TargetType = "local:DelayButton">
<Setter Property = "FocusVisualStyle"
Value = "{StaticResource FocusVisual}" />
<Setter Property = "Background"
Value = "{StaticResource Button.Static.Background}" />
<Setter Property = "BorderBrush"
Value = "{StaticResource Button.Static.Border}" />
<Setter Property = "Foreground"
Value = "{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property = "BorderThickness"
Value = "1" />
<Setter Property = "HorizontalContentAlignment"
Value = "Center" />
<Setter Property = "VerticalContentAlignment"
Value = "Center" />
<Setter Property = "Padding"
Value = "1" />
<Setter Property = "Template">
<Setter.Value>
<ControlTemplate TargetType = "{x:Type local:DelayButton}">
<Border x:Name = "border"
Background = "{TemplateBinding Background}"
BorderBrush = "{TemplateBinding BorderBrush}"
BorderThickness = "{TemplateBinding BorderThickness}"
SnapsToDevicePixels = "true">
<Grid>
<ContentPresenter x:Name = "contentPresenter"
Focusable = "False"
HorizontalAlignment = "{TemplateBinding HorizontalContentAlignment}"
Margin = "{TemplateBinding Padding}"
RecognizesAccessKey = "True"
SnapsToDevicePixels = "{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment = "{TemplateBinding VerticalContentAlignment}" />
<ProgressBar x:Name = "PART_ProgressBar"
Opacity = "0.5"
Value = "0"
Background = "Transparent"
Foreground = "{TemplateBinding ProgressBrush}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property = "IsDefaulted"
Value = "true">
<Setter Property = "BorderBrush"
TargetName = "border"
Value = "{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
</Trigger>
<Trigger Property = "IsMouseOver"
Value = "true">
<Setter Property = "Background"
TargetName = "border"
Value = "{StaticResource Button.MouseOver.Background}" />
<Setter Property = "BorderBrush"
TargetName = "border"
Value = "{StaticResource Button.MouseOver.Border}" />
</Trigger>
<Trigger Property = "IsPressed"
Value = "true">
<Setter Property = "Background"
TargetName = "border"
Value = "{StaticResource Button.Pressed.Background}" />
<Setter Property = "BorderBrush"
TargetName = "border"
Value = "{StaticResource Button.Pressed.Border}" />
</Trigger>
<Trigger Property = "IsEnabled"
Value = "false">
<Setter Property = "Background"
TargetName = "border"
Value = "{StaticResource Button.Disabled.Background}" />
<Setter Property = "BorderBrush"
TargetName = "border"
Value = "{StaticResource Button.Disabled.Border}" />
<Setter Property = "TextElement.Foreground"
TargetName = "contentPresenter"
Value = "{StaticResource Button.Disabled.Foreground}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Я объясняю, почему мне нужна кнопка с командой запуска с задержкой, потому что некоторые команды кнопок должны запускаться осторожно, но не нуждаются в dailog (1-й диалог будет удерживать программу или скрывать некоторую область пользовательского интерфейса, второй dailog нужно, чтобы человек читал больше и нажимал больше, если Я говорю человеку, что нужно просто нажать на какую-нибудь специальную кнопку в стиле пользовательского интерфейса, хаха)
«поскольку я установил DelayInMilliсекунды = 1000», не только нажимаю с помощью 1 с, но и отпускаю кнопку мыши, затем запускаю команду. Я бы хотел просто нажимать с помощью 1 с, а затем запускать команду». - Извините, но я вас не понял. Прямо сейчас мне нужно нажать кнопку на 1 секунду, после чего она выполнит команду (все еще нажимая). Вам не нужно отпускать кнопку. Вот как обстоят дела на данный момент. Означает ли это, что вы хотите удерживать 1 с, затем отпустить и затем выполнить команду? Вы хотите сработать при выпуске? Затем просто установите Button.ClickMode
на ClickMode.Release
: <DelayButton ClickMode = "Release" ... />
.
<local:DelayButton Height = "50" Margin = "10" Content = "Change Id 1000" DelayInMilli Seconds = "1000" Command = "{Binding ChangeIdCommand}"/> Ваш код Я проверяю его, если использовать привязку команды, нужно освободить кнопка.
Хорошо спасибо. Я посмотрю. Пожалуйста, подождите.
Хорошо. Все работало корректно (без ошибок). Дело в том, что по умолчанию для Button.ClickMode
установлено значение Release
. Когда я тестировал код, я явно установил для него значение Pressed
(я тестировал все режимы). Теперь я установил ClickMode
на Pressed
из конструктора, поэтому вам не нужно выполнять дополнительную настройку. Я также добавил анимацию прогресса, которая показывает, как долго пользователю придется удерживать кнопку, пока не начнется щелчок. Скопируйте полную новую версию и добавьте стили в файл /Themes/.Generic.xaml вашего проекта. Пожалуйста, дайте мне знать, если возникнут какие-либо проблемы.
@ZhuYajun Для полноты картины я добавил поддержку клавиатуры. Теперь нажатие кнопки с использованием пробела или ввода также вызовет задержку и анимацию. И не забудьте настроить пространства имен в стилях (Genric.xaml).
Спасибо, вы добавляете стили в визуальное представление, это очень приятно при его использовании. Но есть несколько проблем: если я нажму времени недостаточно, но команда все равно сработает. Если я использую клавиатуру пробела, вы щелкнете в любом месте, чтобы вызвать команду . Для удобства мы можем изменить код здесь:github.com/ZhuYajunFly/WpfApp2.git
Спасибо за ценный отзыв и потраченное время на создание репозитория. Я обновил ответ, чтобы показать последнюю версию DelayButton. Если есть еще ошибки, пожалуйста, дайте мне знать. Если вы довольны, сообщите мне, чтобы мы могли удалить этот разговор. Я добавил больше логики для управления потоком и изоляции путей ввода. Теперь все должно работать так, как ожидалось.
Чтобы обеспечить правильную настройку, я рекомендую создать папку «/Themes» в корне вашего проекта и добавить в нее файл «Generic.xaml». Затем объедините файл DelayButton.xaml (словарь ресурсов) со словарем Generic.xaml.
Замечательно! Почти идеально, если добавить две кнопки задержки или больше кнопок, вы обнаружите, что только при нажатии на них достаточно времени появится «Фокус», затем используйте клавишу пробела, чтобы активировать его, в противном случае активируйте старую кнопку, нажмите кнопку достаточно времени. Вы можете быстро проверьте это github.com/ZhuYajunFly/WpfApp2.git
Я обновил код. Пожалуйста, попробуйте еще раз (добавлен вызов Focus() и улучшены некоторые детали).
Удивительно и идеально! Спасибо BionicCode, вы создали его гениально (прогресс с задержкой). Я думаю, что все больше и больше людей захотят его использовать. Я предлагаю вам отправить этот код в MaterialDesignInXamlToolkit и HandyControl, возможно, это поможет большему количеству людей! Еще раз спасибо, BionicCode.
Спасибо, BionicCode. Ваш код хорош, я тестирую, в нем есть небольшая ошибка, например, я установил DelayInMilli Seconds = "1000", не только нажал 1 с, но и отпустил кнопку мыши, а затем выполнил команду Trigger. Я бы хотел просто нажать 1 с, а затем команду Trigger.