web-dev-qa-db-fra.com

Comment afficher le raccourci clavier de travail pour les éléments de menu?

J'essaie de créer une barre de menus WPF localisable avec des éléments de menu comportant des raccourcis clavier - et non touches de raccourci/mnémoniques (généralement représentés par des caractères soulignés sur lesquels vous pouvez appuyer directement pour sélectionner directement un menu. lorsque le menu est déjà ouvert), mais des raccourcis clavier (généralement des combinaisons de Ctrl + another key) qui sont affichés alignés à droite à côté de l'en-tête de l'élément de menu.

J'utilise le modèle MVVM pour mon application, ce qui signifie que j'évite de placer du code dans code-behind autant que possible et que j'ai mes modèles de vue (que j'assigne à DataContext properties ) fournit des implémentations de ICommand interface qui sont utilisées par les contrôles de mes vues.


Pour reproduire le problème, voici un code source minimal pour une application telle que décrite:

Window1.xaml

<Window x:Class="MenuShortcutTest.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MenuShortcutTest" Height="300" Width="300">
    <Menu>
        <MenuItem Header="{Binding MenuHeader}">
            <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}"/>
        </MenuItem>
    </Menu>
</Window>

Window1.xaml.cs

using System;
using System.Windows;

namespace MenuShortcutTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            this.DataContext = new MainViewModel();
        }
    }
}

MainViewModel.cs

using System;
using System.Windows;
using System.Windows.Input;

namespace MenuShortcutTest
{
    public class MainViewModel
    {
        public string MenuHeader {
            get {
                // in real code: load this string from localization
                return "Menu";
            }
        }

        public string DoSomethingHeader {
            get {
                // in real code: load this string from localization
                return "Do Something";
            }
        }

        private class DoSomethingCommand : ICommand
        {
            public DoSomethingCommand(MainViewModel owner)
            {
                if (owner == null) {
                    throw new ArgumentNullException("owner");
                }

                this.owner = owner;
            }

            private readonly MainViewModel owner;

            public event EventHandler CanExecuteChanged;

            public void Execute(object parameter)
            {
                // in real code: do something meaningful with the view-model
                MessageBox.Show(owner.GetType().FullName);
            }

            public bool CanExecute(object parameter)
            {
                return true;
            }
        }

        private ICommand doSomething;

        public ICommand DoSomething {
            get {
                if (doSomething == null) {
                    doSomething = new DoSomethingCommand(this);
                }

                return doSomething;
            }
        }
    }
}

Le WPF MenuItem class a InputGestureText propriété , mais comme décrit dans SO questions telles que this , this , this et this , cela est purement esthétique et n’a aucun effet sur les raccourcis réellement traités par l’application.

Les questions SO telles que this et this indiquent que la commande doit être liée à un KeyBinding dans la liste InputBindings == de la fenêtre. Bien que cette fonctionnalité soit activée, le raccourci associé à l'élément de menu n'est pas automatiquement affiché. Window1.xaml change comme suit:

<Window x:Class="MenuShortcutTest.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MenuShortcutTest" Height="300" Width="300">
    <Window.InputBindings>
        <KeyBinding Key="D" Modifiers="Control" Command="{Binding DoSomething}"/>
    </Window.InputBindings>
    <Menu>
        <MenuItem Header="{Binding MenuHeader}">
            <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}"/>
        </MenuItem>
    </Menu>
</Window>

J'ai essayé de définir manuellement la propriété InputGestureText en plus, en faisant Window1.xaml ressembler à ceci:

<Window x:Class="MenuShortcutTest.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MenuShortcutTest" Height="300" Width="300">
    <Window.InputBindings>
        <KeyBinding Key="D" Modifiers="Control" Command="{Binding DoSomething}"/>
    </Window.InputBindings>
    <Menu>
        <MenuItem Header="{Binding MenuHeader}">
            <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}" InputGestureText="Ctrl+D"/>
        </MenuItem>
    </Menu>
</Window>

Ceci affiche le raccourci, mais n’est pas une solution viable pour des raisons évidentes:

  • Elle ne se met pas à jour lorsque la liaison de raccourci réelle change. Par conséquent, même si les raccourcis ne sont pas configurables par les utilisateurs, cette solution est un cauchemar pour la maintenance.
  • Le texte doit être localisé (comme par exemple le Ctrl clé a des noms différents dans certaines langues). Par conséquent, si un des raccourcis est modifié, , toutes les traductions devront être mises à jour individuellement.

J'ai envisagé de créer un IValueConverter à utiliser pour lier la propriété InputGestureText à la InputBindings liste de la fenêtre (il pourrait y en avoir plus KeyBinding dans la liste InputBindings, voire aucune, il n’y a donc aucune instance spécifique de KeyBinding à laquelle je puisse me lier (si KeyBinding se prête même à être une cible de liaison)). Cela me semble la solution la plus souhaitable, car elle est à la fois très souple et propre (elle ne nécessite pas une pléthore de déclarations à différents endroits), mais d’une part, InputBindingCollection n'implémente pas INotifyCollectionChanged , la liaison ne sera donc pas mise à jour lorsque les raccourcis seront remplacés. Par contre, je n'ai pas réussi à fournir au convertisseur un référence à mon modèle de vue d'une manière ordonnée (dont il aurait besoin d'accéder aux données de localisation). De plus, InputBindings n'est pas une propriété de dépendance, je ne peux donc pas le lier à une source commune (telle qu'une liste de liaisons d'entrée située dans le modèle de vue) que le ItemGestureText la propriété pourrait également être liée.

Maintenant, beaucoup de ressources ( cette question , cette question , ce fil , cette question et ce fil précisez que RoutedCommand et RoutedUICommand contient un élément intégré InputGestures property et implique que les raccourcis clavier de cette propriété sont automatiquement affichés dans les éléments de menu.

Cependant, l'utilisation de l'une de ces implémentations ICommand semble ouvrir une nouvelle boîte de vers, car leurs méthodes Execute et CanExecute ne sont pas virtuelles. et ne peuvent donc pas être remplacés dans les sous-classes pour remplir la fonctionnalité désirée. La seule façon de fournir cela semble être de déclarer un CommandBinding en XAML (indiqué par exemple ici ou ici ) qui connecte une commande à un gestionnaire d'événements. Toutefois, ce gestionnaire d'événements serait alors situé dans le code-behind, violant ainsi l'architecture de MVVM décrite ci-dessus.


En essayant néanmoins, cela signifie retourner la plupart de la structure susmentionnée (ce qui implique aussi en quelque sorte que je dois décider comment résoudre le problème à un stade précoce de mon développement):

Window1.xaml

<Window x:Class="MenuShortcutTest.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:MenuShortcutTest"
    Title="MenuShortcutTest" Height="300" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:DoSomethingCommand.Instance}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <Menu>
        <MenuItem Header="{Binding MenuHeader}">
            <MenuItem Header="{Binding DoSomethingHeader}" Command="{x:Static local:DoSomethingCommand.Instance}"/>
        </MenuItem>
    </Menu>
</Window>

Window1.xaml.cs

using System;
using System.Windows;

namespace MenuShortcutTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            this.DataContext = new MainViewModel();
        }

        void CommandBinding_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            ((MainViewModel)DataContext).DoSomething();
        }
    }
}

MainViewModel.cs

using System;
using System.Windows;
using System.Windows.Input;

namespace MenuShortcutTest
{
    public class MainViewModel
    {
        public string MenuHeader {
            get {
                // in real code: load this string from localization
                return "Menu";
            }
        }

        public string DoSomethingHeader {
            get {
                // in real code: load this string from localization
                return "Do Something";
            }
        }

        public void DoSomething()
        {
            // in real code: do something meaningful with the view-model
            MessageBox.Show(this.GetType().FullName);
        }
    }
}

DoSomethingCommand.cs

using System;
using System.Windows.Input;

namespace MenuShortcutTest
{
    public class DoSomethingCommand : RoutedCommand
    {
        public DoSomethingCommand()
        {
            this.InputGestures.Add(new KeyGesture(Key.D, ModifierKeys.Control));
        }

        private static Lazy<DoSomethingCommand> instance = new Lazy<DoSomethingCommand>();

        public static DoSomethingCommand Instance {
            get {
                return instance.Value;
            }
        }
    }
}

Pour la même raison (RoutedCommand.Execute et tel étant non virtuel), je ne sais pas comment sous-classer RoutedCommand de manière à créer un RelayCommand comme celui utilisé dans une réponse à cette question basé sur RoutedCommand, je n'ai donc pas à faire le détour par la InputBindings de la fenêtre - lors de la réimplémentation explicite des méthodes de ICommand dans une sous-classe RoutedCommand, j'ai l'impression que je risque de casser quelque chose.

De plus, alors que le raccourci est automatiquement affiché avec cette méthode telle que configurée dans RoutedCommand, il ne semble pas se localiser automatiquement. Je crois comprendre que l’ajout de

System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-de");
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture;

dans le constructeur MainWindow, vous devez vous assurer que les chaînes localisables fournies par le cadre proviennent de l'allemand CultureInfo. Toutefois, Ctrl ne change pas en Strg. À moins que je ne me trompe sur la définition du CultureInfo pour les chaînes fournies par le cadre, cette méthode n’est de toute façon pas viable si je pense que le raccourci affiché sera correctement localisé.

Maintenant, je suis conscient que KeyGesture me permet de spécifier une chaîne d'affichage personnalisée pour le raccourci clavier, mais non seulement la classe RoutedCommand- dérivée DoSomethingCommand est-elle disjointe de toutes mes instances (de où je pourrais entrer en contact avec la localisation chargée) en raison de la façon dont CommandBinding doit être lié à une commande en XAML, la propriété respective de DisplayString est en lecture seule, aucun moyen de le changer quand une autre localisation est chargée au moment de l'exécution.

Cela me laisse avec la possibilité de creuser manuellement dans l’arborescence du menu (EDIT: pour plus de clarté, pas de code ici car je ne le demande pas et je sais comment faire cela) et la liste InputBindings de la fenêtre pour vérifier laquelle les commandes sont associées à toutes les instances KeyBinding et aux éléments de menu liés à ces commandes, de sorte que je puisse définir manuellement le InputGestureText de chacun des éléments de menu respectifs pour refléter le premier (ou le préféré, quelle que soit la métrique de mon choix) utiliser ici) raccourci clavier. Et cette procédure devra être répétée chaque fois que je pense que les raccourcis clavier ont peut-être changé. Cependant, cela semble être une solution de contournement extrêmement fastidieuse pour quelque chose qui est essentiellement une fonctionnalité de base d'une interface graphique de barre de menus, donc je suis convaincu que cela ne peut pas être la "bonne" façon de procéder.

Quel est le bon moyen d'afficher automatiquement un raccourci clavier configuré pour fonctionner avec les instances WPF MenuItem?

EDIT: Toutes les autres questions que j’ai trouvées traitaient de la manière dont un KeyBinding/KeyGesture pouvait être utilisé pour activer la fonctionnalité implicitement visible de InputGestureText, sans expliquer comment lier automatiquement les deux aspects de la situation décrite. La seule question quelque peu prometteuse que j'ai trouvée était this , mais elle n'a reçu aucune réponse depuis plus de deux ans.

19
O. R. Mapper

Je vais commencer par l'avertissement. Il peut arriver que vous ayez besoin non seulement de touches de raccourci personnalisables, mais également du menu lui-même. Alors réfléchissez bien avant d’utiliser InputBindings de manière statique.
Il y a encore une mise en garde concernant InputBindings: ils impliquent que la commande est liée à l'élément dans l'arborescence visuelle de la fenêtre. Parfois, vous avez besoin de touches de raccourci globales non liées à une fenêtre particulière.

Ce qui précède signifie que vous pouvez en faire une autre méthode et implémenter votre propre traitement des gestes d'application avec un routage correct vers les commandes correspondantes (n'oubliez pas d'utiliser des références faibles aux commandes). 

Néanmoins, l'idée de commandes tenant compte des gestes est la même.

public class CommandWithHotkey : ICommand
{
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        MessageBox.Show("It Worked!");
    }

    public KeyGesture Gesture { get; set; }

    public string GestureText
    {
        get { return Gesture.GetDisplayStringForCulture(CultureInfo.CurrentUICulture); }
    }

    public string Text { get; set; }

    public event EventHandler CanExecuteChanged;

    public CommandWithHotkey()
    {
        Text = "Execute Me";

        Gesture = new KeyGesture(Key.K, ModifierKeys.Control);
    }
}

Modèle de vue simple:

public class ViewModel
{
    public ICommand Command { get; set; }

    public ViewModel()
    {
        Command = new CommandWithHotkey();
    }
}

La fenêtre:

<Window x:Class="CommandsWithHotKeys.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:commandsWithHotKeys="clr-namespace:CommandsWithHotKeys"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <commandsWithHotKeys:ViewModel/>
    </Window.DataContext>
    <Window.InputBindings>
        <KeyBinding Command="{Binding Command}" Key ="{Binding Command.Gesture.Key}" Modifiers="{Binding Command.Gesture.Modifiers}"></KeyBinding>
    </Window.InputBindings>
    <Grid>
        <Menu HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="Auto">
            <MenuItem Header="Test">
                <MenuItem InputGestureText="{Binding Command.GestureText}" Header="{Binding Command.Text}" Command="{Binding Command}">
                </MenuItem>
            </MenuItem>
        </Menu>
    </Grid>
</Window>

Bien sûr, vous devriez en quelque sorte charger les informations de mouvements de la configuration, puis initier des commandes avec les données.

La prochaine étape est la saisie de code comme dans VS: Ctrl + K, Ctrl + D, la recherche rapide donne ceci SO question .

14
Pavel Voronin

Si je n'ai pas mal compris votre question, essayez ceci:

<Window.InputBindings>
    <KeyBinding Key="A" Modifiers="Control" Command="{Binding ClickCommand}"/>
</Window.InputBindings>
<Grid >
    <Button Content="ok" x:Name="button">
        <Button.ContextMenu>
        <local:CustomContextMenu>
            <MenuItem Header="Click"  Command="{Binding ClickCommand}"/>
        </local:CustomContextMenu>
            </Button.ContextMenu>
    </Button>
</Grid>

..avec:

public class CustomContextMenu : ContextMenu
{
    public CustomContextMenu()
    {
        this.Opened += CustomContextMenu_Opened;
    }

    void CustomContextMenu_Opened(object sender, RoutedEventArgs e)
    {
        DependencyObject obj = this.PlacementTarget;
        while (true)
        {
            obj = LogicalTreeHelper.GetParent(obj);
            if (obj == null || obj.GetType() == typeof(Window) || obj.GetType() == typeof(MainWindow))
                break;
        }

        if (obj != null)
            SetInputGestureText(((Window)obj).InputBindings);
        //UnSubscribe once set
        this.Opened -= CustomContextMenu_Opened;
    }

    void SetInputGestureText(InputBindingCollection bindings)
    {
        foreach (var item in this.Items)
        {
            var menuItem = item as MenuItem;
            if (menuItem != null)
            {
                for (int i = 0; i < bindings.Count; i++)
                {
                    var keyBinding = bindings[i] as KeyBinding;
                    //find one whose Command is same as that of menuItem
                    if (keyBinding!=null && keyBinding.Command == menuItem.Command)//ToDo : Apply check for None Modifier
                        menuItem.InputGestureText = keyBinding.Modifiers.ToString() + " + " + keyBinding.Key.ToString();
                }
            }
        }
    }
}

J'espère que cela vous donnera une idée.

4
ethicallogics

Voici comment cela s'est passé:

Dans l'événement chargé de ma fenêtre, je fais correspondre les liaisons de commande des éléments de menu avec les liaisons de commande de tous les InputBindings, un peu comme la réponse de ethicallogics, mais pour une barre de menu et elle compare les liaisons de commande et pas seulement le valeur, parce que cela n'a pas fonctionné pour moi. ce code aussi revient dans les sous-menus .

    private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
    {
        // add InputGestures to menu items
        SetInputGestureTextsRecursive(MenuBar.Items, InputBindings);
    }

    private void SetInputGestureTextsRecursive(ItemCollection items, InputBindingCollection inputBindings)
    {
        foreach (var item in items)
        {
            var menuItem = item as MenuItem;
            if (menuItem != null)
            {
                if (menuItem.Command != null)
                {
                    // try to find an InputBinding with the same command and take the Gesture from there
                    foreach (KeyBinding keyBinding in inputBindings.OfType<KeyBinding>())
                    {
                        // we cant just do keyBinding.Command == menuItem.Command here, because the Command Property getter creates a new RelayCommand every time
                        // so we compare the bindings from XAML if they have the same target
                        if (CheckCommandPropertyBindingEquality(keyBinding, menuItem))
                        {
                            // let a new Keygesture create the String
                            menuItem.InputGestureText = new KeyGesture(keyBinding.Key, keyBinding.Modifiers).GetDisplayStringForCulture(CultureInfo.CurrentCulture);
                        }
                    }
                }

                // recurse into submenus
                if (menuItem.Items != null)
                    SetInputGestureTextsRecursive(menuItem.Items, inputBindings);
            }
        }
    }

    private static bool CheckCommandPropertyBindingEquality(KeyBinding keyBinding, MenuItem menuItem)
    {
        // get the binding for 'Command' property
        var keyBindingCommandBinding = BindingOperations.GetBindingExpression(keyBinding, InputBinding.CommandProperty);
        var menuItemCommandBinding = BindingOperations.GetBindingExpression(menuItem, MenuItem.CommandProperty);

        if (keyBindingCommandBinding == null || menuItemCommandBinding == null)
            return false;

        // commands are the same if they're defined in the same class and have the same name
        return keyBindingCommandBinding.ResolvedSource == menuItemCommandBinding.ResolvedSource
            && keyBindingCommandBinding.ResolvedSourcePropertyName == menuItemCommandBinding.ResolvedSourcePropertyName;
    }

Faites cela une fois dans le code-behind de votre fenêtre et chaque élément de menu a un InputGesture. Il manque juste la traduction

1
JCH2k

Basé sur la réponse de Pavel Voronin, j'ai créé ce qui suit. En fait, je viens de créer deux nouveaux UserControls qui définissent automatiquement Gesture sur la commande et la lisent. 

class HotMenuItem : MenuItem
{
    public HotMenuItem()
    {
        SetBinding(InputGestureTextProperty, new Binding("Command.GestureText")
        {
            Source = this
        });
    }
}

class HotKeyBinding : KeyBinding
{

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property.Name == "Command" || e.Property.Name == "Gesture")
        {
            if (Command is IHotkeyCommand hotkeyCommand)
                hotkeyCommand.Gesture = Gesture as KeyGesture;
        }
    }
}

L'interface utilisée 

public interface IHotkeyCommand
{
    KeyGesture Gesture { get; set; }
}

La commande est à peu près la même chose, elle implémente simplement INotifyPropertyChanged.

Donc, l'utilisation devient un peu plus propre à mon avis: 

<Window.InputBindings>
    <viewModels:HotKeyBinding Command="{Binding ExitCommand}" Gesture="Alt+F4" />
</Window.InputBindings>

<Menu>
    <MenuItem Header="File" >
        <viewModels:HotMenuItem Header="Exit"  Command="{Binding ExitCommand}" />
    </MenuItem>
</Menu>
0
Matt