Я пытаюсь создать собственный раскрывающийся список, который действует как ComboBox, так что всплывающее окно открывается, когда вы щелкаете мышью вниз (не вверх), и закрывается, когда вы щелкаете за пределами элемента управления.
Проблема в том, что он работает только в том случае, если я установил для ClickMode значение «Release». Но то, что я действительно хочу, это ClickMode="Press", чтобы всплывающее окно открывалось на MouseDown вместо MouseUp.
Но когда я устанавливаю его в ClickMode="Press", всплывающее окно не закрывается, когда вы щелкаете за пределами элемента управления.
Любые идеи, как я могу этого добиться?
Использование :
<StackPanel>
<local:CustomDropdown Width = "200"
Height = "50"
Content = "Custom!" />
<ComboBox Width = "200"
Margin = "20">
<ComboBoxItem>A</ComboBoxItem>
<ComboBoxItem>B</ComboBoxItem>
<ComboBoxItem>C</ComboBoxItem>
</ComboBox>
</StackPanel>
Сорт :
internal class CustomDropdown : ContentControl
{
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(CustomDropdown), new PropertyMetadata(false));
}
Ксамл:
<Style TargetType = "{x:Type local:CustomDropdown}">
<Style.Setters>
<Setter Property = "Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<ToggleButton IsChecked = "{Binding IsOpen, Mode=TwoWay, RelativeSource = {RelativeSource TemplatedParent}}"
ClickMode = "Press"/>
<ContentPresenter Content = "{Binding Content, RelativeSource = {RelativeSource TemplatedParent}}"
HorizontalAlignment = "Center"
VerticalAlignment = "Center"/>
<Popup StaysOpen = "False"
Placement = "Bottom"
IsOpen = "{Binding IsOpen, Mode=TwoWay, RelativeSource = {RelativeSource TemplatedParent}}">
<Border Background = "White"
BorderBrush = "Black"
BorderThickness = "1"
Padding = "50">
<TextBlock Text = "Popup!" />
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
Если вы хотите, чтобы с ClickMode.Press
он работал должным образом, вы должны программно установить для свойства IsOpen
значение false
всякий раз, когда вы хотите закрыть Popup
. Например, всякий раз, когда вы обнаруживаете щелчок за пределами ToggleButton
.
Например, вы можете обработать событие PreviewMouseLeftButtonDown
для родительского окна в вашем элементе управления. Что-то вроде этого:
internal class CustomDropdown : ContentControl
{
private ToggleButton _toggleButton;
public CustomDropdown()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_toggleButton = GetTemplateChild("toggleButton") as ToggleButton;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Window window = Window.GetWindow(this);
window.PreviewMouseLeftButtonDown += OnWindowPreviewMouseLeftButtonDown;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Window window = Window.GetWindow(this);
window.PreviewMouseLeftButtonDown -= OnWindowPreviewMouseLeftButtonDown;
}
private void OnWindowPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ToggleButton toggleButton = FindParent<ToggleButton>(e.OriginalSource as DependencyObject);
if (toggleButton != _toggleButton)
IsOpen = false;
}
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null)
return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(CustomDropdown), new PropertyMetadata(false));
}
}
XAML:
<ControlTemplate>
<Grid>
<ToggleButton x:Name = "toggleButton" ...
У вас уже есть рабочий ответ. Однако нахождение родителя Window
и родителя ToggleButton
может повлиять на производительность (в зависимости от глубины визуального дерева).
В качестве альтернативного решения я предлагаю сосредоточиться на работе с Popup
.
Есть два условия, которые препятствуют закрытию Popup
: кнопка настроена с ButtonBase.ClickMode
, установленным на ClickMode.Pressed
, И пользователь не нажимает ничего, на что можно сфокусироваться внутри всплывающего окна.
Если одно из этих двух условий оценивается как ложное (=> ClickMode.Release
или пользователь переместил фокус внутрь Popup
), ваш код будет работать так, как вы ожидали.
Обратите внимание, что для того, чтобы пользователь мог перемещать фокус внутри Popup
, должен быть дочерний элемент, который можно сфокусировать (для UIElement.Focusable
установлено значение true
— это false
по умолчанию для большинства элементов управления, не требующих взаимодействия с пользователем). Например, TextBlock
по умолчанию не фокусируется.
Поскольку вы хотите, чтобы кнопка была настроена на вызов события Click
при нажатии кнопки мыши, вам придется перемещать фокус вручную. Но когда вы устанавливаете его вручную, Popup
не получит щелчок мыши, чтобы настроить себя для наблюдения за фокусом. Таким образом, вы в конечном итоге закроете Popup
вручную (отняв соответствующий контроль у Popup
).
В следующем примере элемент Popup
закрывается, наблюдая за событием Mouse.PreviewMouseDownOutsideCapturedElement
, чтобы определить, когда фокус сместился с элемента управления CustomDropdown
(щелчок мыши за пределами Popup
):
CustomDropdown.cs
internal class CustomDropdown : ContentControl
{
public bool IsOpen
{
get => (bool)GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(
"IsOpen",
typeof(bool),
typeof(CustomDropdown),
new PropertyMetadata(default(bool), OnIsOpenChanged));
public CustomDropdown()
{
Mouse.AddPreviewMouseDownOutsideCapturedElementHandler(this, OnPreviewMouseDownOutsideCapturedElement);
}
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool isOpen = (bool)e.NewValue;
if (isOpen)
{
_ = Mouse.Capture(d as IInputElement, CaptureMode.SubTree);
}
else
{
_ = Mouse.Capture(null);
}
}
// Manually close the Popup if click is recorded outside the CustomDropdown/Popup
private void OnPreviewMouseDownOutsideCapturedElement(object sender, MouseButtonEventArgs e)
{
SetCurrentValue(IsOpenProperty, false);
}
}
Вы должны нажать на что-то, что может получить фокус. Свойство StaysOpen
управляет поведением отслеживания фокуса. Если фокус перемещается из всплывающего окна, а для StaysOpen
установлено значение false
, всплывающее окно закроется. Фон окна не принимает фокус. Обычно пользователь нажимает на другой элемент пользовательского интерфейса, например на кнопку. Обычно всплывающее окно открывается поверх других элементов пользовательского интерфейса.
Я ценю это, но я пытаюсь получить поведение, которое действует как ComboBox, где вы можете щелкнуть в любом месте снаружи, даже на нефокусируемом элементе, таком как фон окна. Я только что просматривал справочный источник для встроенного WPF ComboBox, и они используют Mouse.Capture(comboBox, CaptureMode.SubTree). Так что я думаю, что это путь, который я попробую. Проблема в том, что они используют множество скрытых внутренних вспомогательных методов Microsoft.
Я знаю реализацию. Мой пример в основном делает то же самое: находит фокусируемый элемент и перемещает фокус. Захват мыши используется для генерации события щелчка. Он отслеживает мышь и захватывает мышь. Это делается для того, чтобы не пропустить событие подъема мыши в случае, если пользователь отводит мышь от поля со списком (пока кнопка все еще нажата) и отпускает ее, находясь над каким-либо другим элементом управления.
@wforl Я наконец-то обновил решение. Теперь это чисто реализовано. Теперь я ловлю мышь и вместо этого управляю Mouse.PreviewMouseDownOutsideCapturedElement
. Я бы сказал, что текущее решение действительно чистое и как бы имитирует исходное поведение/реализацию ComboBox. Я также удалил необязательную логику для перемещения фокуса. теперь код стал намного компактнее и чище (количество строк кода сократилось до нескольких). Просто дайте мне знать, если вам нужна дополнительная помощь.
вау, хорошая работа. Теперь я отметил это как ответ. Хотя выглядит подозрительно просто!? чего не хватает? :) ... кстати, есть ли причина не использовать ReleaseMouseCapture()? вместо Mouse.Capture(null)?
Спасибо. Простота делает решение таким элегантным, на мой взгляд. Нет, как вы, наверное, уже знаете, ReleaseMouseCapture — это просто оболочка над Mouse.Capture(null). Я не хотел смешивать API. Вот и все.
Одна проблема, которую я заметил в моем конкретном случае, заключалась в том, что мне нужен ReleaseMouseCapture(); когда IsOpen изменяется на false (например, когда выбрано значение), в противном случае мышь захватывается до тех пор, пока вы не щелкнете что-то еще
Хорошая точка зрения. Я переместил релиз захвата с OnPreviewMouseDownOutsideCapturedElement
на OnIsOpenChanged
. Спасибо за помощь в улучшении кода.
Ваш пример, кажется, не работает для меня. Если я нажму за пределами всплывающего окна (например, фон главного окна), оно не закроет всплывающее окно.