Создание горячих клавиш на C# Windows.Forms (примерно как режим рации в Discord)

Всем доброго времени суток. Столкнулась с проблемой того, что пыталась написать приложение и там есть две функции которые связаны с тем, чтобы пользователь мог юзать горячие клавиши(бинды) для использования некоторого функционала. KeyPreview = true; применяла конечно. Не понимаю почему не получается забиндить комбинации клавиш и клавиши модификаторы по типу Ctrl + G или отдельно Ctrl/Shift etc. Вот пример того, что у меня получилось, но работает косо криво:

    private string bind1;
    private string bind2;
    private InputSimulator inputSimulator = new InputSimulator();

    public RebinderForm()
    {
        InitializeComponent();
        this.Load += RebinderForm_Load;
        this.KeyPreview = true;
        this.bindBox.KeyPress += bindBox_KeyPress;
        this.actionBox.KeyPress += actionBox_KeyPress;
    }

    private void RebinderForm_Load(object sender, EventArgs e)
    {
    }

    private void bindBox_KeyPress(object sender, KeyPressEventArgs e)
    {
        char keyChar = e.KeyChar;

        if (!char.IsControl(keyChar))
        {
            bindBox.Text = keyChar.ToString();
            bind1 = keyChar.ToString();
        }

        e.Handled = true;
    }

    private void actionBox_KeyPress(object sender, KeyPressEventArgs e)
    {
        char keyChar = e.KeyChar;

        if (!char.IsControl(keyChar))
        {
            actionBox.Text = keyChar.ToString();
            bind2 = keyChar.ToString();
        }

        e.Handled = true;
    }

    private void ExecuteAction()
    {
        inputSimulator.Keyboard.ModifiedKeyStroke(
            WindowsInput.Native.VirtualKeyCode.CONTROL,
            WindowsInput.Native.VirtualKeyCode.VK_A);
    }
}

}


Ответы (2 шт):

Автор решения: xellan

Создание сочетания клавиш и привязка их в пользовательском интерфейсе. Для всех форм где используется отлов горячих клавиш устанавливаем KeyPreview = true; Всё остальное я привязал через конструктор, все события и т.п Для TextBox(сов) ставим у ReadOnly значение false чтобы запретить ввод пользователю и меняем BackColor цвет фона по своему желанию, курсор (Cursor) я установил значением Hand

Создаем первую форму где будем проверять комбинации клавиш введите сюда описание изображения

Код Form1

using System.Diagnostics;

namespace Binding_HotKey_Exemple
{
    public partial class Form1 : Form
    {
        public static List<HotKeysArrange> HotKeysLis = new(); //Список со всевозможными комбинациями, сделан статическим чтобы можно было задать его из других форм
        public List<Keys?> KeysListTemp = new(); //Временный список нажатых клавиш, нужен для проверки что было нажато.
        public Form1()
        {
            InitializeComponent();
            KeyPreview = true;
        }
        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            Debug.WriteLine(e.KeyCode);
            if (!KeysListTemp.Contains(e.KeyCode) && HotKeysLis.Any(hotKey => hotKey.OneKey == e.KeyCode)) //Если первая нажатая клавиша равна первой клавиши любой комбинации, добавляем её
                KeysListTemp.Add(e.KeyCode);

            if (!KeysListTemp.Contains(e.KeyCode) && HotKeysLis.Any(hotKey => hotKey.TwoKey == e.KeyCode)) //Если вторая нажатая клавиша равна второй клавиши любой комбинации, добавляем её
                KeysListTemp.Add(e.KeyCode);

            if (!KeysListTemp.Contains(e.KeyCode) && HotKeysLis.Any(hotKey => hotKey.FreeKey == e.KeyCode)) //Если третья нажатая клавиша равна третьей клавиши любой комбинации, добавляем её
                KeysListTemp.Add(e.KeyCode);

            #region Проверка нажатых клавиш.
            if (KeysListTemp.Count == 1) // Проверяем что в нажатых клавишах для отлова находится одна клавиша
            {
                var temp = HotKeysLis.Where(hotKey => KeysListTemp.Contains(hotKey.OneKey) && hotKey.TwoKey == null && hotKey.FreeKey == null).FirstOrDefault();
                if (temp != null)
                {
                    temp.Action(); //Вызываем наш привязанный метод
                    KeysListTemp.Clear();
                }
            }

            if (KeysListTemp.Count == 2) // Проверяем что в нажатых клавишах для отлова находятся две клавиши
            {
                var temp = HotKeysLis.Where(hotKey => KeysListTemp.Contains(hotKey.OneKey) && KeysListTemp.Contains(hotKey.TwoKey) && hotKey.FreeKey == null).FirstOrDefault();
                if (temp != null)
                {
                    temp.Action();  //Вызываем наш привязанный метод
                    KeysListTemp.Clear();
                }
            }

            if (KeysListTemp.Count == 3) // Проверяем что в нажатых клавишах для отлова находятся три клавиши
            {
                var temp = HotKeysLis.Where(hotKey => KeysListTemp.Contains(hotKey.OneKey) && KeysListTemp.Contains(hotKey.TwoKey) && KeysListTemp.Contains(hotKey.FreeKey)).FirstOrDefault();
                if (temp != null)
                {
                    temp.Action(); //Вызываем наш привязанный метод
                    KeysListTemp.Clear();
                }
            }
            #endregion
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e) => KeysListTemp.Clear(); //Если пользователь отпустил клавишу, удаляем все данные из списка.

        #region Методы для которых будут задаваться комбинации клавиш
        public void OneMessage() => MessageBox.Show(label1.Text);
        public void TwoMessage() => MessageBox.Show(label2.Text);
        public void FreeMessage() => MessageBox.Show(label3.Text);
        #endregion

        #region Передача методов для привязки их к горячим клавишам
        private void textBox1_Click(object sender, EventArgs e)
        {
            BindingHotkeyForm form = new(OneMessage, textBox1);
            form.Show();
        }

        private void textBox2_Click(object sender, EventArgs e)
        {
            BindingHotkeyForm form = new(TwoMessage, textBox2);
            form.Show();
        }

        private void textBox3_Click(object sender, EventArgs e)
        {
            BindingHotkeyForm form = new(FreeMessage, textBox3);
            form.Show();
        }
        #endregion
    }
    /// <summary>
    /// Модель для горячих клавиш, я ограничился тремя сочетаниями комбинаций
    /// </summary>
    public class HotKeysArrange
    {
        public Action Action { get; set; } //Наш метод для привязки к комбинации клавиш
        public Keys? OneKey { get; set; }

        public Keys? TwoKey { get; set; }

        public Keys? FreeKey { get; set; }
    }
}

Создаём вторую форму BindingHotkeyForm введите сюда описание изображения

Код BindingHotkeyForm

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Binding_HotKey_Exemple
{
    public partial class BindingHotkeyForm : Form
    {
        public Action action;
        HotKeysArrange TempHotKeysArrange = new HotKeysArrange(); //Временная модель для установки горячих клавиш
        private TextBox textBox; //Передаём  textBox из главной формы чтобы отобразить в нем комбинацию клавиш.

        public BindingHotkeyForm(Action action, TextBox textBox)
        {
            InitializeComponent();
            KeyPreview = true;
            this.action = action;
            TempHotKeysArrange.Action = action; //Устанавливаем метод который будет работать с заданной комбинацией клавиш
            this.textBox = textBox;
        }
        private void BindingHotkeyForm_KeyDown(object sender, KeyEventArgs e)
        {
            if (TempHotKeysArrange.OneKey == null)
            {
                TempHotKeysArrange.OneKey = e.KeyCode;
                textBox1.Text += e.KeyCode.ToString();
                textBox1.Select(textBox1.Text.Length, 0);
                return;
            }
            if (TempHotKeysArrange.TwoKey == null)
            {
                TempHotKeysArrange.TwoKey = e.KeyCode;
                textBox1.Text += $" + {e.KeyCode}";
                textBox1.Select(textBox1.Text.Length, 0);
                return;
            }
            if (TempHotKeysArrange.FreeKey == null)
            {
                TempHotKeysArrange.FreeKey = e.KeyCode;
                textBox1.Text += $" + {e.KeyCode}";
                textBox1.Select(textBox1.Text.Length, 0); //Устанавливаем курсор в конец строки, не красиво выглядит когда курсов мигает при каждом изменение в начале....
                return;
            }
        }
        private void button2_Click(object sender, EventArgs e) => Close(); // В случае отмены закрываем форму

        private void button1_Click(object sender, EventArgs e)
        {
            textBox.Text = $"{(TempHotKeysArrange.OneKey != null ? TempHotKeysArrange.OneKey : "")}" +
                           $"{(TempHotKeysArrange.TwoKey != null ? " + " + TempHotKeysArrange.TwoKey : "")}" +
                           $"{(TempHotKeysArrange.FreeKey != null ? " + " + TempHotKeysArrange.FreeKey : "")}";
            textBox.Select(textBox.Text.Length, 0);
            Form1.HotKeysLis.RemoveAll(x => x.Action == action); //Удаляем старые комбинации клавиш для данной привязки
            Form1.HotKeysLis.Add(TempHotKeysArrange); // Добавляем новое сочетание клавиш
            Close();
        }
    }
}

Результат: введите сюда описание изображения

Проект можно изучить по ссылке https://github.com/xellans/exemple/tree/main/Binding_HotKey-Exemple

→ Ссылка
Автор решения: EvgeniyZ

Что-то в ответе ранее куча лишнего и довольно костыльного, когда все это сводится к нескольким строкам кода. Частично это дубликат ответа (ну а как без них, основано все на одном ведь) от xellan, но вместо написания кучи комментариев думаю, а дай напишу по своему)

  • Для начала давайте сделаем класс, который будет иметь в себе название и действие кнопки. Так, как действие у нас нечто абстрактное, то мы смело можем брать Action. В итоге получаем к примеру такое:

    public record HotKeyInfo(string Name, Action? Action);
    
  • Далее нам нужно место, где мы будем хранить все наши хоткеи. Обычно на одну горячую клавишу вешается один обработчик, а это значит, что нам отлично подойдет Dictionary, ключом которого будет служить стандартный Keys, который является простым Enum, да еще и помечем как флаг, что позволяет делать битовые операции. В итоге получаем такое:

    public Dictionary<Keys, HotKeyInfo> HotKeys { get; private set; } = [];
    
  • Дальше нам нужно получить текущие нажатые клавиши формы, а именно событие OnKeyDown. Пишем в классе формы override, жмем пробел и выбираем из предложенного списка OnKeyDown, готово, у нас появился такой код, где в e.KeyData будут наши кнопки:

    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);
    }
    
  • Теперь наша цель взять из словаря описание хоткея (в случае наличия такого там) и вызвать его Action. Делается это элементарно:

    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);
    
        if (HotKeys.TryGetValue(e.KeyData, out var hotKeyInfo))
        {
            hotKeyInfo.Action?.Invoke();
        }
    }
    
  • Поздравляю, осталось лишь заполнить словарь данными. Например, напишем такое в конструкторе окна:

    HotKeys.Add(Keys.Control | Keys.A, new("Тестовая команда 1", ()=> { Debug.WriteLine($"Мы нажали { Name }"); }));
    HotKeys.Add(Keys.Control | Keys.Shift | Keys.D, new("Тестовая команда 2", ()=> { Debug.WriteLine("Мм, а это команда 2"); }));
    
  • Запускаем, жмем клавиши и видим как в окне отладки появляется то, что мы указали.

    Result

Собственно вот вам универсальный и довольно простой способ сделать команды в вашем приложении. Дальше уже подключайте фантазию, развивайте эту идею как хотите. Хотите делать интерфейс, пожалуйста, сохраните последнее сочетание клавиш все через тот-же OnKeyDown и сделайте HotKeys.TryAdd(e.KeyData, ...);. Хотите добавить описание, вперед, редактируйте HotKeyInfo под себя и используйте где надо. Ну и так далее.

И да, я надеюсь вы поняли, что Action это условно говоря метод, то есть вы можете там указать лямбду как я()=> что-то;, а можете создать отдельный метод void и уже прописать там просто его имя (.Add(..., new("Команда", OnHotKey1Pressed));). Ну и также вы можете это все кастомизировать под свои нужды, передавая параметры если надо, получая может быть какие либо данные, ну и др.

В общем, вот вам самый простейший пример, изучайте, дорабатывайте, пробуйте. Удачи!


Весь код:

public record HotKeyInfo(string Name, Action? Action);

public partial class Form1 : Form
{
    public Dictionary<Keys, HotKeyInfo> HotKeys { get; private set; } = [];

    public Form1()
    {
        InitializeComponent();

        HotKeys.Add(Keys.Control | Keys.A, new("Тестовая команда 1", ()=> { Debug.WriteLine($"Мы нажали { Name }"); }));
        HotKeys.Add(Keys.Control | Keys.Shift | Keys.D, new("Тестовая команда 2", ()=> { Debug.WriteLine("Мм, а это команда 2"); }));
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);

        if (HotKeys.TryGetValue(e.KeyData, out var hotKeyInfo))
        {
            hotKeyInfo.Action?.Invoke();
        }
    }
}

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

public record HotKeyInfo(string Name, Action? Action);

public partial class Form1 : Form
{
    public Dictionary<int, HotKeyInfo> HotKeys { get; private set; } = [];

    public Form1()
    {
        InitializeComponent();

        HotKeys.Add(ToKeyValues(Keys.ControlKey, Keys.A), new("Тестовая команда 1", ()=> { Debug.WriteLine($"Мы нажали { Name }"); }));
        HotKeys.Add(ToKeyValues(Keys.ShiftKey, Keys.D, Keys.F), new("Тестовая команда 2", ()=> { Debug.WriteLine("Мм, а это команда 2"); }));
    }

    private int ToKeyValues(params Keys[] keys) 
        => keys.Sum(x => (int)x);


    private HashSet<Keys> _pressedKeys = [];
    private HashSet<Keys> _allKeys = [];
    protected override void OnKeyDown(KeyEventArgs e)
    {
        _allKeys.Add(e.KeyCode);
        _pressedKeys.Add(e.KeyCode);
    }

    protected override void OnKeyUp(KeyEventArgs e)
    {
        _allKeys.Remove(e.KeyCode);

        if (_allKeys.Count == 0)
        {
            var hotKeyCode = ToKeyValues([.. _pressedKeys]);

            if (HotKeys.TryGetValue(hotKeyCode, out var hotKeyInfo))
            {
                hotKeyInfo.Action?.Invoke();
            }

            _pressedKeys.Clear();
        }
    }
}

Поясняю:

  • Словарь теперь хранит числовое значение кнопок, их сумму. Если мы зайдем в документацию, то увидим, что к примеру клавиша Ctrl будет иметь код 17, а допустим A будет иметь 65, их сочетание будет 17+65 = 82, это и будет уникальный код нашей комбинации. Такая реализация позволит нажимать клавиши в любой последовательности, да и с любым кол-вом клавиш.

  • ToKeyValues - это вспомогательный метод, который и суммирует код клавиш, отдавая результат.

  • HashSet<Keys> _pressedKeys и HashSet<Keys> _allKeys - Так, как клавиша не может быть нажата дважды, нам нужна коллекция, которая хранит в себе только уникальные значения, ну а это именно HashSet. В _pressedKeys мы будем хранить все нажатые кнопки пользователем до тех пор, пока нажата хоть одна клавиша, в конце это и будет нужное сочетание клавиш. Ну а _allKeys будет неким индикатором, что нажато в данный момент, мы в нее будем добавлять при нажатии и сразу удалять из нее при отжатии клавиши.

  • OnKeyDown - Переопределяем событие, которое происходит в момент зажатия клавиши. В нем мы добавляем нажатую кнопку в наши коллекции.

  • OnKeyUp - Обратное OnKeyDown, выполняется в момент отжатия клавиши. В нем мы удаляем клавишу из _allKeys, и если эта коллекция пуста (все клавиши отжаты) начинаем сверять комбинацию клавиш с нашим списком.

  • Ну а остальное, думаю и так понятно, ибо описывал выше. Мы формируем код нашей комбинации клавиш, ищем его в словаре, если есть такой, то вызываем, ну а если нету, проходим мимо.

Собственно, теперь думаю все проблемы тут решены. Код остался вроде такой-же простой и понятный, без каких либо дубликатов и хардкора на три клавиши) Есть правда одно НО, мы потеряли возможность зажимать комбинацию клавиш, ибо триггер срабатывания у нас "если все клавиши отжаты". Так что, если вам нужно именно "зажимать", то код нужно будет немного переделать, а именно в момент зажатия клавиши подсчитывать код комбинации, искать если есть и выполнять пока клавиша зажата, но это уже пусть будет в виде домашнего задания.

→ Ссылка