Как вы решаете эту проблему LostFocus/LostKeyboardFocus?

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

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

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

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

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

Итак... кто-нибудь знает, как я могу успешно закодировать это поведение?

отметка


person Mark A. Donohoe    schedule 01.05.2011    source источник
comment
Я думаю, что это слишком большой, чтобы прочитать все. Будет неплохо, если вы поместите небольшое резюме, маркеры и пример кода, если сможете. Только если эти вещи будут выглядеть интересно, люди могут подробно прочитать, чтобы ответить вам.   -  person Akash Kava    schedule 01.05.2011
comment
Вам нужны подробности, чтобы понять, о чем я говорю. Мне лично нравятся такие длинные объяснения, поскольку я точно знаю, через что уже прошел программист. Я обнаружил, что когда я этого не делаю, люди продолжают публиковать точные решения, которые я уже пробовал, даже когда я уже объяснил, почему они не будут работать, потому что они не понимали, почему они будут работать' т работать. Проще говоря, это не основная проблема для «нубского» сайта, которую можно свести к пунктам списка, поэтому моя публикация на SO в первую очередь.   -  person Mark A. Donohoe    schedule 01.05.2011
comment
Боже мой... Я всегда думал, что SO освобожден от правила TL;DR - в конце концов, программисты должны уметь читать!   -  person Vladislav Zorov    schedule 02.05.2011
comment
Спасибо, @Владислав Зоров! Не могу не согласиться!   -  person Mark A. Donohoe    schedule 02.05.2011
comment
LostKeyboardFocus также срабатывает, когда вы переключаетесь на другое приложение, что очень раздражает.   -  person Paul McCarthy    schedule 04.02.2020
comment
(KeyboardFocusChangedEventArgs) e.newFocus имеет значение null при переключении приложения, поэтому вы можете проверить это.   -  person Paul McCarthy    schedule 04.02.2020
comment
Да, @PaulMcCarthy, но тогда вы все равно не можете использовать это, поскольку у вас все еще есть проблема с потерей фокуса клавиатуры при появлении контекстного меню. Вот почему вам нужно сочетание этих двух.   -  person Mark A. Donohoe    schedule 04.02.2020


Ответы (6)


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

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

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

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

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

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

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

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

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

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

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

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

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

Обойти это было так, поскольку текстовое поле на самом деле является элементом в шаблоне IsEditing для моего элемента управления, я просто добавил новый DP во внешний элемент управления с именем IsEditingContextMenu, который затем привязал к текстовому полю через внутренний стиль TextBox, затем я добавил 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' для контекстного меню. (Если вы ХОТИТЕ подавить меню, вы все равно не будете использовать «null». Вы должны установить его в пустое меню, поскольку null означает «Использовать значение по умолчанию»)

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

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

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

отметка

person Mark A. Donohoe    schedule 02.05.2011

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

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

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

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

  • Пусть элемент управления остается в состоянии редактирования, когда он имеет логический фокус
  • Добавить явный механизм фиксации или применения
  • Обрабатывайте все запутанные случаи, которые возникают, когда вы пытаетесь поддерживать автоматическую фиксацию.
person Rick Sladkey    schedule 01.05.2011
comment
Я не думаю, что это поведение сложно вообще. Это просто «Выход из режима редактирования, когда вы теряете фокус» с оговоркой, что отображение контекстного меню не означает потерю фокуса. Проводник Windows делал это всегда. Переименуйте файл, и это именно то поведение, которое вы получите... кнопки «Принять/Отменить» не нужны. WinForms тоже был тортом, поскольку контекстные меню не отвлекали внимание. ИМХО, я не согласен (хотя я понимаю, почему) MS заставила контекстное меню перехватывать фокус клавиатуры. Однако они должны были это учитывать. Вы должны сделать 7 левых поворотов, чтобы идти прямо сейчас. - person Mark A. Donohoe; 02.05.2011
comment
@MarqueIV: На самом деле это довольно сложно, даже у DataGrid есть некоторые проблемы с этим. Если у вас есть DataGrids в DataTemplate TabControl, и вы переключаете вкладки во время редактирования, вы получаете исключение из-за неправильной логики редактирования. - person H.B.; 02.05.2011
comment
@MarquelIV: Но это не проводник Windows и не WinForms. Я не защищаю весь дизайн и инфраструктуру WPF; что есть, то есть. Учитывая, что это то, что есть, у вас есть три варианта. - person Rick Sladkey; 02.05.2011
comment
Но самое замечательное в том, чтобы быть программистом... выяснить, как сделать что-то с тем, что у вас есть, а не с тем, что вы хотите. Это как получать деньги за решение головоломок весь день, и время от времени вы получаете что-то действительно креативное и неожиданное. Вот почему я спрашиваю ТАК, удалось ли кому-нибудь еще повторить это поведение и как они его обошли. То, что у вас есть отвертка, а не молоток, не означает, что вы не можете использовать ручку, чтобы забивать гвозди. Оптимальный? Нет. Но это лучше, чем просто перевернуться и сказать, что ты не можешь этого сделать. - person Mark A. Donohoe; 02.05.2011
comment
О, и @Rick Sladkey, я не верю, что когда-либо говорил, что считаю, что ответ прост. Опять же, не пришел бы сюда, если бы это было. ТАК для лучших из лучших, чтобы показать свои вещи, а не «нубские» форумы с людьми, которые читают TYS WPF за 21 день. (Кроме того, какое отношение имеет «Это не проводник» к чему-либо? Это изученное поведение ОС, которое вы хотите воспроизвести для согласованности. Это просто обеспечивает отличный пользовательский интерфейс, точно так же, как вы инстинктивно понимаете, что F2 обычно означает переименовать .) - person Mark A. Donohoe; 02.05.2011
comment
@MarqueIV: я извиняюсь за то, что предположил, что вы искали простое решение. Я должен был сказать, что мы все хотели бы найти более простое решение. - person Rick Sladkey; 02.05.2011
comment
@ Рик Слэдки, НП. Но вернемся к вопросу... можете ли вы придумать, как "обрабатывать все возникающие запутанные дела..."? Мои предложения выглядят многообещающе? Можете ли вы придумать какие-либо предостережения или другие возможности здесь? Нам действительно нужно такое поведение, поскольку люди (наши клиенты) уже привыкли к нему как в Windows, так и в наших устаревших приложениях, отличных от WPF. - person Mark A. Donohoe; 02.05.2011
comment
@MarqueIV: Вы на правильном пути. Да, я потерял фокус клавиатуры, но это потому, что контекстное меню открыто. Мне пришлось бы закодировать образец, чтобы убедиться, что логика верна. Когда вы сделаете это правильно, ваши клиенты не будут думать дважды, но вы будете знать, через какие обручи вам пришлось прыгать. - person Rick Sladkey; 02.05.2011
comment
Привет, @Rick Sladkey ... Я вижу, вы также прокомментировали мой другой вопрос в аналогичной теме о контекстных меню по умолчанию, который, как я понял, хотя и пометил его как ответ, на самом деле не был. В частности, как узнать, когда будет отображаться контекстное меню по умолчанию для текстового поля? ContextMenuOpening срабатывает только тогда, когда вы явно устанавливаете свой собственный. Если я смогу это выяснить (за исключением создания собственного), то у меня есть решение проблемы в этом вопросе. (Даже если я могу каким-то образом явно назначить контекстному меню его значение по умолчанию, это тоже может сработать, поскольку ContextMenu не будет нулевым.) - person Mark A. Donohoe; 02.05.2011
comment
@MarqueIV: Ты упустил свой шанс. У вас был один из самых умных парней во всей стране WPF, который отвечал на ваш вопрос. Что касается ContextMenuOpening, вы можете получить его от TextBox и переопределить OnContextMenuOpening, чтобы перехватить это событие. - person Rick Sladkey; 02.05.2011
comment
@ Рик Слэдки ... кого ты имел в виду? И что я пропустил? И вы уверены, что можете перехватить это событие, даже если меню нет? Не уверен, какой подход был бы лучше... создание подкласса текстового поля или его стиль, как я. - person Mark A. Donohoe; 02.05.2011
comment
@MarqueIV: Джош Смит — одна из икон сообщества WPF. Он ответил на вопрос, на который, как вы сказали, на самом деле не было ответа. Если у вас когда-либо была возможность ответить на ваш вопрос, это был он. Кстати, я знаю, что вы немного огорчились из-за этого вопроса, поэтому я рад, что вы поднялись над дракой и сосредоточились на проблеме. - person Rick Sladkey; 02.05.2011
comment
Спасибо @Rick Sladkey! Честно говоря, сегодня был мой первый негативный опыт здесь. Самое смешное, что @H.W. также был одним из тех, кто там отвечал, так что он уже знает, что я склонен много писать. Это просто стиль. Люди не обязаны соглашаться. Моя точка зрения заключалась в том, что это не имело отношения к вопросу и не продвигало его (согласно часто задаваемым вопросам здесь), поэтому я не понял драмы. Ну и что! Если слишком длинно, не читайте! Это было мое мнение. Разные удары, я думаю. Кстати, даже дольше, чем мой вопрос, был моим ответом. Вы проверяли это? Мысли? Я хотел бы обратной связи. Мне это кажется «хакерским», но это работает! - person Mark A. Donohoe; 02.05.2011

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

    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...
    }
person Saragani    schedule 11.05.2016
comment
К сожалению, нет, потому что, если вы откроете контекстное меню, вы потеряете фокус клавиатуры и вернетесь. Если вы затем щелкнете в другом месте приложения или в другом приложении, у вас не будет других уведомлений для выхода из режима редактирования. Однако ваш код может упростить хак контекстного меню, описанный выше, где я изменяю стиль элемента управления, хотя цель размещения становится фактором, поскольку она может быть установлена ​​в другом месте. Тем не менее, хорошая пища для размышлений. Спасибо! - person Mark A. Donohoe; 11.05.2016

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

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

person JonnyRaa    schedule 01.02.2013
comment
Проблема с этим подходом в том, что он не учитывает навигацию только с помощью клавиатуры. Тем не менее, хорошая информация для других. Спасибо! - person Mark A. Donohoe; 02.02.2013
comment
Я собираюсь попробовать добавить навигацию по вкладкам в свои элементы управления, так что, возможно, скоро почувствую вашу боль! Есть ли способы потерять фокус через клавиатуру, где вы не получаете события нажатия клавиш? Контекстные меню в wpf вообще немного кошмарны - они кажутся пронизанными ошибками, поэтому меня не удивляет, что они вызывают проблемы и здесь. - person JonnyRaa; 05.02.2013
comment
Конечно! Каждый раз, когда вы выполняете системную клавишу (например, Alt-Tab), вы не получите уведомление. Что ж, вы получите полууведомление, так как увидите «Alt», но не получите «Tab», когда отключитесь. Тем не менее, описанный выше подход (хотя и запутанный) работает и для этих случаев, поскольку он основан как на обычном LostFocus, так и на фокусе клавиатуры, поэтому, если вы будете следовать ему, вы должны быть золотыми даже в вашем случае. ХТХ! - person Mark A. Donohoe; 06.02.2013

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

person gauravvc    schedule 05.02.2015
comment
Можете ли вы подробнее рассказать об этом, возможно, с примерами кода, чтобы лучше объяснить, что вы имеете в виду и что это решило? - person Mark A. Donohoe; 07.02.2015

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

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

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

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

person TheDark    schedule 23.06.2015
comment
Разве это не нарушает клавиатурную навигацию по контекстным меню? Помните, их можно запускать с клавиатуры. - person Mark A. Donohoe; 24.06.2015
comment
Вы правы, в моем случае это не имеет значения, потому что клавиатуры нет. - person TheDark; 30.06.2015
comment
Не могли бы вы просто установить в контекстном меню пустую коллекцию вместо null? Кроме того, я думаю, что есть событие, которое вы можете перехватить и обработать, которое в первую очередь остановит его открытие. - person Mark A. Donohoe; 29.09.2020