Как вы решаете эту проблему LostFocus / LostKeyboardFocus? - PullRequest
10 голосов
/ 01 мая 2011

Хорошо, у меня есть элемент управления, который имеет свойство IsEditing, которое для аргумента аргумента имеет шаблон по умолчанию, который обычно является текстовым блоком, но когда IsEditing имеет значение true, он заменяется текстовым полем для редактирования на месте.Теперь, когда элемент управления теряет фокус, если он все еще редактирует, он должен выйти из режима редактирования и вернуться обратно в шаблон TextBlock.Довольно прямо, верно?

Подумайте о поведении переименования файла в Windows Explorer или на вашем рабочем столе (это то же самое, что я знаю ...) Это поведение, которое мы хотим.

Проблема в том, что вы не можете использовать событие LostFocus, потому что при переключении на другое окно (или элемент, который является FocusManager) LostFocus не срабатывает, поскольку элемент управления все еще имеет логический фокус, поэтому он не будет работать.

Если вы вместо этого используете LostKeyboardFocus, в то время как это решает проблему «другого FocusManager», теперь у вас есть новая: когда вы редактируете и нажимаете правой кнопкой мыши на текстовое поле, чтобы отобразить контекстное меню, потому что контекстное менютеперь имеет фокус клавиатуры, ваш элемент управления теряет фокус клавиатуры, выходит из режима редактирования и закрывает контекстное меню, сбивая пользователя с толку!

Теперь я попытался установить флаг, чтобы игнорировать LostKeyboardFocus перед открытием меню,затем с помощью этого фига в событии LostKeyboardFocus определить, выкинуть его из режима редактирования или нет, но если менюПосле этого я нажимаю в другом месте приложения, так как сам элемент управления больше не фокусируется на клавиатуре (меню имело это), элемент управления никогда не получает другое событие LostKeyboardFocus, поэтому он остается в режиме редактирования.(Возможно, мне придется добавить проверку, когда меню закрывается, чтобы увидеть, что находится в фокусе, а затем вручную вывести его из режима редактирования, если это не элемент управления. Это кажется многообещающим.)Я могу успешно закодировать это поведение?

Mark

Ответы [ 6 ]

9 голосов
/ 02 мая 2011

Хорошо ... это было "весело", как в "Программистах". Реальная боль в кайстере, чтобы понять, но с милой огромной улыбкой на лице, которую я сделал. (Пора взять IcyHot для моего плеча, учитывая, что я сам так сильно похлопал по нему!: P)

В любом случае, это многошаговая вещь, но удивительно простая, когда вы все выясните. Короткая версия - вам нужно использовать оба LostFocus и LostKeyboardFocus, а не одно или другое.

LostFocus легко. Всякий раз, когда вы получаете это событие, установите IsEditing в false. Сделано и сделано.

Контекстные меню и фокус потерянной клавиатуры

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

Чтобы обработать это поведение, вы переопределяете ContextMenuOpening (или обрабатываете событие) и устанавливаете флаг уровня класса, указывающий, что меню открывается. (Я использую bool _ContextMenuIsOpening.) Затем в переопределении (или событии) LostKeyboardFocus вы проверяете этот флаг и, если он установлен, вы просто очищаете его и больше ничего не делаете. Однако, если он не установлен, это означает, что что-то кроме открытия контекстного меню приводит к тому, что элемент управления теряет фокус клавиатуры, поэтому в этом случае вы хотите установить для IsEditing значение false.

Уже открытые контекстные меню

Теперь существует странное поведение: если контекстное меню для элемента управления открыто, и, таким образом, элемент управления уже потерял фокус клавиатуры, как описано выше, если вы щелкнете в другом месте приложения, прежде чем новый элемент управления получит фокус, ваш элемент управления получит Сначала фокус клавиатуры, но только на долю секунды, а затем мгновенно возвращает его новому элементу управления.

Это на самом деле работает в наших интересах, так как это означает, что мы также получим еще одно событие LostKeyboardFocus, но на этот раз флаг _ContextMenuOpening будет установлен в значение false, и, как описано выше, наш обработчик LostKeyboardFocus затем установит IsEditing в ложь, что именно то, что мы хотим. Я люблю счастливое приключение!

Теперь просто переместил фокус на элемент управления, на который вы щелкнули, без предварительной настройки фокуса обратно на элемент управления, владеющий контекстным меню, а затем нам нужно было бы сделать что-то вроде перехвата события ContextMenuClosing и проверки того, какой элемент управления будет если мы сфокусируемся на следующем, тогда мы установим IsEditing на false, только если элемент управления, который скоро будет сфокусирован, не был тем, который породил контекстное меню, поэтому мы в основном избежали пули.

Предупреждение: контекстные меню по умолчанию

Теперь есть также предостережение о том, что если вы используете что-то вроде текстового поля и явно не устанавливаете в нем свое собственное контекстное меню, то вы не получаете событие ContextMenuOpening, что удивило мне. Однако это легко исправить, просто создав новое контекстное меню с теми же стандартными командами, что и контекстное меню по умолчанию (например, вырезать, скопировать, вставить и т. Д.) И назначив его текстовому полю. Он выглядит точно так же, но теперь вы получаете событие, необходимое для установки флага.

Однако даже в этом случае у вас есть проблема, как будто вы создаете сторонний многократно используемый элемент управления, и пользователь этого элемента управления хочет иметь свое собственное контекстное меню, вы можете случайно установить для своего более высокий приоритет, и вы ' переопределим их!

Обходной путь был, так как текстовое поле на самом деле является элементом в шаблоне IsEditing для моего элемента управления, я просто добавил новый DP на внешний элемент управления под названием IsEditingContextMenu, который затем связал с текстовым полем через внутренний * Стиль 1057 *, затем я добавил DataTrigger в этом стиле, который проверяет значение IsEditingContextMenu на внешнем элементе управления, и если оно пустое, я устанавливаю меню по умолчанию, которое я только что создал выше и которое хранится в ресурсе.

Вот внутренний стиль для текстового поля (элемент с именем 'Root' представляет внешний элемент управления, который пользователь фактически вставляет в свой XAML) ...

<Style x:Key="InlineTextbox" TargetType="TextBox">

    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="FocusVisualStyle"      Value="{x:Null}" />
    <Setter Property="ContextMenu"           Value="{Binding IsEditingContextMenu, ElementName=Root}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">

                <Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>

            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <DataTrigger Binding="{Binding IsEditingContextMenu, RelativeSource={RelativeSource AncestorType=local:EditableTextBlock}}" Value="{x:Null}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Command="ApplicationCommands.Cut" />
                        <MenuItem Command="ApplicationCommands.Copy" />
                        <MenuItem Command="ApplicationCommands.Paste" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

</Style>

Обратите внимание, что вы должны установить начальную привязку контекстного меню в стиле, а не непосредственно в текстовом поле, иначе DataTrigger стиля будет заменен непосредственно установленным значением, что сделает триггер бесполезным, и вы сразу вернетесь в квадрат, есличеловек использует 'null' для контекстного меню.(Если вы ХОТИТЕ скрыть меню, вы все равно не будете использовать «ноль». Вы установите пустое меню, так как ноль означает «Использовать по умолчанию»)

Так что теперь пользователь может использоватьобычное свойство ContextMenu, когда IsEditing равно false ... они могут использовать IsEditingContextMenu, когда IsEditing имеет значение true, и если они не указали IsEditingContextMenu, для текстового поля используется определенное нами внутреннее значение по умолчанию.Поскольку контекстное меню текстового поля на самом деле никогда не может быть нулевым, его ContextMenuOpening всегда срабатывает, и поэтому логика для поддержки этого поведения работает.

Как я уже сказал ... НАСТОЯЩАЯ боль в банке, вычисляющей все это,но, черт побери, если у меня нет действительно крутого чувства выполненного долга здесь.

Я надеюсь, что это поможет другим здесь с такой же проблемой.Не стесняйтесь отвечать здесь или пишите мне с вопросами.

Mark

3 голосов
/ 02 мая 2011

К сожалению, вы ищете простое решение сложной проблемы. Поставленная проблема просто заключается в том, чтобы иметь интеллектуальные элементы управления пользовательским интерфейсом с автоматической фиксацией, которые требуют минимального взаимодействия и "делают правильные вещи", когда вы "переключаетесь" с них.

Причина, по которой это сложно, в том, что правильная вещь зависит от контекста приложения. Подход, используемый WPF, заключается в том, чтобы дать вам логическую направленность и концепции фокусировки на клавиатуре и позволить вам решить, как поступить правильно в вашей ситуации.

Что если откроется контекстное меню? Что должно произойти, если меню приложения открыто? Что если фокус переключится на другое приложение? Что, если открыто всплывающее окно, принадлежащее местному контролю Что если пользователь нажимает клавишу ввода, чтобы закрыть диалоговое окно? Все эти ситуации могут быть обработаны, но все они исчезают, если у вас есть кнопка фиксации или пользователь должен нажать Enter для подтверждения.

Итак, у вас есть три варианта:

  • Оставить элемент управления в состоянии редактирования, когда он имеет логический фокус
  • Добавить явный механизм фиксации или применения
  • Обработка всех беспорядочных случаев, возникающих при попытке поддержки автоматической фиксации
1 голос
/ 11 мая 2016

Не было бы проще:

    void txtBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        TextBox txtBox = (sender as TextBox);

        if (e.NewFocus is ContextMenu && (e.NewFocus as ContextMenu).PlacementTarget == txtBox)
        {
            return;
        }

        // Rest of code for existing edit mode here...
    }
0 голосов
/ 23 июня 2015

Я прошел здесь в поисках решения для аналогичной проблемы: у меня есть ListBox, который теряет фокус, когда открывается ContextMenu, и я не хочу, чтобы это произошло.

Моим простым решением было установить Focusable на False, как для ContextMenu, так и для MenuItem s:

<ContextMenu x:Key="QueryResultsMenu" Focusable="False">
    <ContextMenu.Resources>
        <Style TargetType="MenuItem">
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </ContextMenu.Resources>
    <MenuItem ... />
</ContextMenu>

Надеюсь, это поможет будущим искателям ...

0 голосов
/ 05 февраля 2015

Не уверен, но это может быть полезно.У меня была похожая проблема с редактируемой комбинированной коробкой.Моя проблема была в том, что я использовал метод переопределения OnLostFocus, который не вызывался.Исправлено: я добавил обратный вызов к событию LostFocus, и все работало нормально.

0 голосов
/ 01 февраля 2013

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

см. Ответ здесь: Как элемент управления может обрабатывать щелчок мыши за пределами этого элемента управления?

...