Пользовательский элемент управления всплывающим окном не закрывается

Я пытаюсь создать собственный раскрывающийся список, который действует как 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>
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вы хотите, чтобы с 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);
  }
}

Ваш пример, кажется, не работает для меня. Если я нажму за пределами всплывающего окна (например, фон главного окна), оно не закроет всплывающее окно.

wforl 01.02.2023 16:23

Вы должны нажать на что-то, что может получить фокус. Свойство StaysOpen управляет поведением отслеживания фокуса. Если фокус перемещается из всплывающего окна, а для StaysOpen установлено значение false, всплывающее окно закроется. Фон окна не принимает фокус. Обычно пользователь нажимает на другой элемент пользовательского интерфейса, например на кнопку. Обычно всплывающее окно открывается поверх других элементов пользовательского интерфейса.

BionicCode 01.02.2023 16:34

Я ценю это, но я пытаюсь получить поведение, которое действует как ComboBox, где вы можете щелкнуть в любом месте снаружи, даже на нефокусируемом элементе, таком как фон окна. Я только что просматривал справочный источник для встроенного WPF ComboBox, и они используют Mouse.Capture(comboBox, CaptureMode.SubTree). Так что я думаю, что это путь, который я попробую. Проблема в том, что они используют множество скрытых внутренних вспомогательных методов Microsoft.

wforl 01.02.2023 16:41

Я знаю реализацию. Мой пример в основном делает то же самое: находит фокусируемый элемент и перемещает фокус. Захват мыши используется для генерации события щелчка. Он отслеживает мышь и захватывает мышь. Это делается для того, чтобы не пропустить событие подъема мыши в случае, если пользователь отводит мышь от поля со списком (пока кнопка все еще нажата) и отпускает ее, находясь над каким-либо другим элементом управления.

BionicCode 01.02.2023 17:50

@wforl Я наконец-то обновил решение. Теперь это чисто реализовано. Теперь я ловлю мышь и вместо этого управляю Mouse.PreviewMouseDownOutsideCapturedElement. Я бы сказал, что текущее решение действительно чистое и как бы имитирует исходное поведение/реализацию ComboBox. Я также удалил необязательную логику для перемещения фокуса. теперь код стал намного компактнее и чище (количество строк кода сократилось до нескольких). Просто дайте мне знать, если вам нужна дополнительная помощь.

BionicCode 01.02.2023 23:06

вау, хорошая работа. Теперь я отметил это как ответ. Хотя выглядит подозрительно просто!? чего не хватает? :) ... кстати, есть ли причина не использовать ReleaseMouseCapture()? вместо Mouse.Capture(null)?

wforl 02.02.2023 13:47

Спасибо. Простота делает решение таким элегантным, на мой взгляд. Нет, как вы, наверное, уже знаете, ReleaseMouseCapture — это просто оболочка над Mouse.Capture(null). Я не хотел смешивать API. Вот и все.

BionicCode 02.02.2023 14:20

Одна проблема, которую я заметил в моем конкретном случае, заключалась в том, что мне нужен ReleaseMouseCapture(); когда IsOpen изменяется на false (например, когда выбрано значение), в противном случае мышь захватывается до тех пор, пока вы не щелкнете что-то еще

wforl 02.02.2023 14:20

Хорошая точка зрения. Я переместил релиз захвата с OnPreviewMouseDownOutsideCapturedElement на OnIsOpenChanged. Спасибо за помощь в улучшении кода.

BionicCode 02.02.2023 14:23

Другие вопросы по теме