Adicionando suporte a atalhos de teclado ao WPF XamOutlookBar
Se você é um usuário do Microsoft Outlook, deve ter notado que pode navegar para os diferentes grupos do Outlook usando atalhos de teclado simples. Nesta postagem do blog, explicaremos como adicionar facilmente o suporte a atalhos de teclado ao WPF XamOutlookBar. Leia mais.
Recentemente, recebi uma pergunta sobre o suporte a atalhos de teclado para alterar grupos no Infragistics WPF xamOutlookBar. Por exemplo, você pode pressionar CRTL+2 para navegar até o grupo Calendário ou CRTL+3 para navegar até o grupo Contatos. A ideia aqui é que um usuário possa usar o teclado para navegar pelos grupos contidos na navegação da barra do Outlook. Infelizmente, descobri que nosso xamOutlookBar do WPF não suportava esse comportamento. Assim começou minha busca para encontrar uma solução. Na verdade, eu vim com duas soluções. Vou descrevê-los e você pode decidir qual abordagem você mais gosta.
Os requisitos
Antes de terminarmos com a codificação de soluções, provavelmente devemos falar sobre como esse recurso deve funcionar.
- O primeiro requisito óbvio é que devemos ser capazes de alterar grupos selecionados dentro do controle xamOutlookBar usando atalhos de teclado.
- Quero ser capaz de atribuir quais modificadores de tecla (CTRL, ALT, SHIFT) e combinação de teclas do teclado (A, B, C) invocarão a mudança de grupos.
- Quero ser capaz de atribuir várias combinações de gestos de tecla a um único grupo. Portanto, se eu quiser que CTRL+1 e SHIFT+1 naveguem para o mesmo grupo, isso deve ser suportado.
- Eu conheço os problemas que você pode ter com eventos de foco e teclado no WPF. Portanto, é importante que, independentemente de qual controle tenha foco ou onde eu esteja na tela, eu possa alterar os grupos de barras do Outlook com minha combinação de teclas de atalho.
Isso é sobre cobri-lo. Vamos à resolução de problemas!
Abordagem um para atalhos de teclado
A primeira abordagem que adotei foi aproveitar os InputBindings internos do WPF. Aqui, definirei vários atalhos de teclado no nível da janela, porque quero que os atalhos sejam executados, não importa onde eu esteja na minha visão ou o que tenha foco. Isso significa que vou precisar de um ICommand para vincular aos meus KeyBindings e invocar a alteração dos grupos, dependendo da minha combinação de teclas. O problema é que esse comando precisará conhecer os modificadores de tecla, a tecla pressionada e ter acesso a todos os grupos no controle xamOutlookBar. Portanto, posso dar ao meu comando todos esses itens se usar o próprio objeto KeyBinding como o CommandParameter. Armazenarei o próprio controle xamOutlookBar por meio da propriedade CommandTarget. O que me dará acesso a tudo o que preciso. Também usarei os InputBindings internos para atribuir minhas combinações/gestos de atalho a um OutlookBarGroup. Vamos dar uma olhada no código.
<Window.InputBindings> <KeyBinding Modifiers="Control" Key="D1" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="NumPad1" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="D2" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="NumPad2" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="D3" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="NumPad3" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="D4" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> <KeyBinding Modifiers="Control" Key="NumPad4" Command="{Binding ChangeGroupCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" CommandTarget="{Binding ElementName=xamOutlookBar1}"/> </Window.InputBindings>
Como você pode ver, defini vários KeyBindings no elemento Window.InputBindings. Como quero mudar de grupo usando números, tenho que lidar com os números que correm ao longo da parte superior do teclado, bem como com os números no teclado numérico do teclado. Isso significa que para cada número terei dois KeyBindings. Estou associando dados meu KeyBinding a um Command definido em um ViewModel chamado ChangeGroupCommand. O CommandParamter será a instância do objeto KeyBinding. Por fim, o CommandTarget será o controle xamOutlookBar. Como o CommandParameter é a instância KeyBinding, agora tenho acesso ao gesto de tecla (modificadores e tecla pressionada), bem como a todos os grupos no controle xamOutlookBar em meu ViewModel. Vamos dar uma olhada rápida nesse VewModel.
public class MainViewModel { public ICommand ChangeGroupCommand { get; set; } public MainViewModel() { ChangeGroupCommand = new RelayCommand<KeyBinding>(x => ChangeGroup(x)); } private void ChangeGroup(KeyBinding keyBinding) { XamOutlookBar outlookBar = keyBinding.CommandTarget as XamOutlookBar; if (outlookBar != null) { foreach (var group in outlookBar.Groups) { foreach (KeyBinding binding in group.InputBindings) { if (binding.Modifiers == keyBinding.Modifiers && binding.Key == keyBinding.Key) { group.IsSelected = true; return; } } } } } }
Esse é um ViewModel muito simples que tem uma única propriedade ICommand definida. O ICommand é uma implementação RelayCommand. Se você não está familiarizado com um RelayCommand, o Google/Bing é seu amigo. O método ChangeGroup usa o parâmetro KeyBinding, pega o controle xamOutlookBar da propriedade CommandTarget e começa a pesquisar cada grupo no xamOutlookBar em busca de grupos que tenham InputBindings que correspondam ao KeyBinding de entrada. Como atribuímos KeyBindings ao OutlookBarGroups? Fácil, assim:
<igWPF:XamOutlookBar x:Name="xamOutlookBar1" HorizontalAlignment="Left"> <igWPF:OutlookBarGroup Header="Group 1"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D1" /> <KeyBinding Modifiers="Control" Key="NumPad1" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 1"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 2"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D2" /> <KeyBinding Modifiers="Control" Key="NumPad2" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 2"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 3"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D3" /> <KeyBinding Modifiers="Control" Key="NumPad3" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 3"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 4"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D4" /> <KeyBinding Modifiers="Control" Key="NumPad4" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 4"/> </igWPF:OutlookBarGroup> </igWPF:XamOutlookBar>
Estamos simplesmente adicionando vários KeyBindings à coleção OutlookBarGroup.InputBindings. Lembre-se, devido às diferentes maneiras de inserir números, temos que adicionar um KeyBinding para cada combinação de teclas. Isso é tudo o que há para fazer. Agora execute o aplicativo e nossos atalhos de teclado funcionarão conforme o esperado. Pressionar CTRL+3 selecionará o OutlookBarGroup para "Grupo 3".

Abordagem Dois
Essa primeira abordagem funcionou muito bem, mas teve alguma duplicação de esforços. Eu não queria ter que mapear meus KeyBindings mais de uma vez. Eu preferiria uma propriedade simples que eu pudesse ligar/desligar e fazer tudo funcionar. Então decidi adotar uma abordagem diferente que envolve um pouco mais de código, mas muito mais flexível e fácil de implementar. Essa segunda abordagem aproveita um AttachedProperty, bem como os KeyBindings internos. Nossos KeyBindings em nossos OutlookBarGroups não mudaram nem um pouco. Esses ficam onde estão. Eles ainda são usados para definir quais gestos de tecla de atalho invocarão a mudança de grupo. Em seguida, vamos criar uma AttachedProperty chamada EnableInputBindings. Vou apenas fornecer o código e depois falar sobre as várias seções.
public class InputBinding : DependencyObject { public static readonly DependencyProperty InputBindingBehaviorProperty = DependencyProperty.RegisterAttached("InputBindingBehavior", typeof(XamOutlookBarKeyBindingBehavior), typeof(InputBinding), new PropertyMetadata(null)); public static readonly DependencyProperty EnableKeyBindingsProperty = DependencyProperty.RegisterAttached("EnableKeyBindings", typeof(bool), typeof(InputBinding), new PropertyMetadata(false, new PropertyChangedCallback(EnableKeyBindingsChanged))); public static bool GetEnableKeyBindings(DependencyObject obj) { return (bool)obj.GetValue(EnableKeyBindingsProperty); } public static void SetEnableKeyBindings(DependencyObject obj, bool value) { obj.SetValue(EnableKeyBindingsProperty, value); } private static void EnableKeyBindingsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { XamOutlookBar outlookBar = d as XamOutlookBar; bool isEnabled = (bool)e.NewValue; if (outlookBar != null) { XamOutlookBarKeyBindingBehavior behavior = GetOrCreateBehavior(outlookBar); if (isEnabled) behavior.Attach(); else behavior.Dettach(); } } private static XamOutlookBarKeyBindingBehavior GetOrCreateBehavior(XamOutlookBar outlookBar) { XamOutlookBarKeyBindingBehavior behavior = outlookBar.GetValue(InputBindingBehaviorProperty) as XamOutlookBarKeyBindingBehavior; if (behavior == null) { behavior = new XamOutlookBarKeyBindingBehavior(outlookBar); outlookBar.SetValue(InputBindingBehaviorProperty, behavior); } return behavior; } } public class XamOutlookBarKeyBindingBehavior : InputBindingBehaviorBase<XamOutlookBar> { Window _parentWindow; public XamOutlookBarKeyBindingBehavior(XamOutlookBar outlookBar) : base(outlookBar) { } public override void Attach() { //since we want to listen for all key events no matter which control has focus, we need to listen at the Window level //otherwise the KeyUp event will never execute if (_parentWindow == null) _parentWindow = Window.GetWindow(TargetObject); if (_parentWindow != null) _parentWindow.AddHandler(Keyboard.KeyUpEvent, (KeyEventHandler)HandleKeyUp, true); } public override void Dettach() { if (_parentWindow != null) _parentWindow.RemoveHandler(Keyboard.KeyUpEvent, (KeyEventHandler)HandleKeyUp); } void HandleKeyUp(object sender, System.Windows.Input.KeyEventArgs e) { try { //We only want to check for shorcuts if we are dealing with modifier keys. if (Keyboard.Modifiers == ModifierKeys.None) return; foreach (OutlookBarGroup group in TargetObject.Groups) { foreach (KeyBinding binding in group.InputBindings) { if (binding.Modifiers == Keyboard.Modifiers && binding.Key == e.Key) { group.IsSelected = true; return; } } } } catch (Exception ex) { Debug.WriteLine(ex.Message); } } } public abstract class InputBindingBehaviorBase<T> where T : UIElement { private readonly WeakReference _targetObject; protected T TargetObject { get { return _targetObject.Target as T; } } public InputBindingBehaviorBase(T targetObject) { _targetObject = new WeakReference(targetObject); } public abstract void Attach(); public abstract void Dettach(); }
O que temos aqui é a criação de uma pequena estrutura InputBinding. Primeiro, temos um AttachedProperty que me permite ligar / desligar as ligações de entrada. Também definimos uma propriedade anexada que simplesmente conterá uma instância de uma classe de comportamento que fornecerá uma funcionalidade chamada InputBindingBehavior.
Quando a propriedade anexada EnableKeyBindings é definida, uma nova instância de classe XamOutlookBarKeyBindingBehavior é criada e armazenada na propriedade anexada InputBindingBehavior. Se o EnableKeyBindings for true, o comportamento será anexado, se for false, será desanexado. se você olhar para a classe XamOutlookBarKeyBindingBehvaior, verá que ela é realmente simples. Ele deriva de uma classe base abstrata chamada InputBindingBehaviorBase<T> que simplesmente contém uma referência fraca ao objeto de destino (nesse caso, o xamOutlookBar). Quando o comportamento é anexado, primeiro obtemos uma instância para o controle Window pai superior. Lembre-se, queremos executar nossos gestos de atalho, não importa o que tenha foco no evento. Depois de termos a instância Window, agora podemos adicionar um manipulador ao evento KeyUp, certificando-nos de especificar que queremos lidar com eventos "manipulados" também. O manipulador KeyUp primeiro garante que estamos lidando com modificadores. Se não tivermos nenhum, não queremos executar a lógica. Se o fizermos, percorra todos os grupos e verifique se há KeyBindings que correspondam ao gesto da tecla de atalho que acabou de ser pressionado. Se houver uma correspondência, selecione o grupo. A última etapa é simplesmente definir a propriedade anexada no controle xamOutlookBar.
<igWPF:XamOutlookBar x:Name="xamOutlookBar1" HorizontalAlignment="Left" local:InputBinding.EnableInputBindings="True"> <igWPF:OutlookBarGroup Header="Group 1"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D1" /> <KeyBinding Modifiers="Control" Key="NumPad1" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 1"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 2"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D2" /> <KeyBinding Modifiers="Control" Key="NumPad2" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 2"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 3"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D3" /> <KeyBinding Modifiers="Control" Key="NumPad3" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 3"/> </igWPF:OutlookBarGroup> <igWPF:OutlookBarGroup Header="Group 4"> <igWPF:OutlookBarGroup.InputBindings> <KeyBinding Modifiers="Control" Key="D4" /> <KeyBinding Modifiers="Control" Key="NumPad4" /> </igWPF:OutlookBarGroup.InputBindings> <Label Content="Content for Group 4"/> </igWPF:OutlookBarGroup> </igWPF:XamOutlookBar>
Como você pode ver, essa abordagem requer um pouco mais de código para ser configurada, mas agora é muito mais fácil habilitar o suporte a gestos de atalho. Basta definir uma única propriedade anexada como True e definir seus KeyBindings em seus grupos e em funcionamento. Mesmo resultado, apenas uma maneira diferente de fazer isso.

Baixe o código-fonte e divirta-se.
Por curiosidade, qual abordagem você prefere?