5

Мне нужно что бы после нажатия левой кнопки выполнялось какое либо действие до отжатия мыши, как я могу это cделать? Пробовал такой способ:

public static System.Windows.Input.MouseButtonState LeftButton { get; }
static void Main()
{
    if (Mouse.LeftButton == MouseButtonState.Pressed)
        UpdateSampleResults("Left Button Pressed");
}

Выдает ошибку: System.InvalidOperationException: "Вызывающим потоком должен быть STA, поскольку этого требуют большинство компонентов UI."

brezz
  • 105
  • я предлагаю отталкиваться от цели приложения. Зачем консольному приложению знать о нажатии ЛКМ, курсоре и пр. ? – Leonid Zolotarov Nov 08 '20 at 14:53
  • Откровенно говоря, подобное я делал в С++, я там в консоль вместо лога выводил, какие клавиши нажимаются, и это очень тормозило приложение – Leonid Zolotarov Nov 08 '20 at 14:54
  • 1
    Вы используете .net framework или .net core? – Andrei Khotko Nov 08 '20 at 19:46

2 Answers2

9

На самом деле, задача для консоли не типичная, и следовательно .NET никак не помогает с решением. То есть готового решения от Microsoft нет, или я его не нашел.

Идея с использованием System.Windows.Input никуда не приведет, потому что вам придется реализовать Windows Message Loop, то есть фактически стартовать GUI приложение с окном, пусть даже не видимым, которое будет принимать сообщения от системы о нажатиях на мышь и клавиатуру. Альтернатива - крутить в цикле проверки нажатий, что выглядит еще хуже как лишняя нагрузка на систему. То есть ваше приложение ничего не делает, но при этом N раз в секунду крутит цикл, чтобы унюхать нажатия на мышь. Следующая проблема - координаты мыши, в Windows.Input там будут пиксели, а если окна нет, то пиксели чего? Задача еще усложнится. То есть даже если с помощью инструментов GUI приложения оно и решаемо, то очень далеко не просто это сделать. Гораздо проще выкинуть консоль и сделать сразу полноценное Windows приложение, например WPF.

Если же оставаться в консоли, то можно использовать P/Invoke методы Windows API, то есть низком уровне стучаться в систему и спрашивать, что же пользователь там делает с мышью у нас в окне. Так и поступим, потому что это как минимум проще, чем то что было предложено вами.

Сейчас будет много кода.

NativeMethods.cs - подключение методов Win API

internal static class NativeMethods
{
    [DllImport("kernel32.dll")]
    public static extern IntPtr GetStdHandle(ConsoleInputHandle nStdHandle);
[DllImport("kernel32.dll")]
public static extern bool GetConsoleMode(IntPtr hConsoleInput, ref ConsoleMode lpMode);

[DllImport("kernel32.dll")]
public static extern bool SetConsoleMode(IntPtr hConsoleInput, ConsoleMode dwMode);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern bool ReadConsoleInput(IntPtr hConsoleInput, [Out] InputRecord[] lpBuffer, int nLength, ref int lpNumberOfEventsRead);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern bool WriteConsoleInput(IntPtr hConsoleInput, InputRecord[] lpBuffer, int nLength, ref int lpNumberOfEventsWritten);

}

И чтобы с этой кучкой методов можно было работать, потребуются следующие энумераторы и структуры

ConsoleStructs.cs

[Flags]
public enum ConsoleMode : uint
{
    None = 0x0000,
    EchoInput = 0x0004,
    WindowInput = 0x0008,
    MouseInput = 0x0010,
    QuickEditMode = 0x0040,
    ExtendedFlags = 0x0080,
}

public enum ConsoleInputHandle : int { StandardInput = -10, StandardOutput = -11, StandardError = -12 }

public struct ConsolePoint { public short X; public short Y; }

[Flags] public enum ConsoleEventType : ushort { Keyboard = 0x0001, Mouse = 0x0002, WindowBufferSize = 0x0004, Menu = 0x0008, Focus = 0x0010 }

[StructLayout(LayoutKind.Explicit)] public struct InputRecord { [FieldOffset(0)] public ConsoleEventType EventType; [FieldOffset(4)] public KeyboardRecord KeyEvent; [FieldOffset(4)] public MouseRecord MouseEvent; [FieldOffset(4)] public WindowBufferSizeRecord WindowBufferSizeEvent; }

[Flags] public enum MouseButtonState : uint { NoButton = 0x0000, LeftButton = 0x0001, RightButton = 0x0002, MiddleButton = 0x0004, ThirdButton = 0x0008, FourthButton = 0x0010 }

[Flags] public enum ControlKeyState : uint { None = 0x0000, RightAlt = 0x0001, LeftAlt = 0x0002, RightCtrl = 0x0004, LeftCtrl = 0x0008, Shift = 0x0010, NumLock = 0x0020, ScrollLock = 0x0040, CapsLock = 0x0080, EnhancedKey = 0x0100, }

[Flags] public enum MouseEventFlagsState : uint { Idle = 0x0000, Move = 0x0001, DoubleClick = 0x0002, Wheel = 0x0004, HorizontalWheel = 0x0008 }

[StructLayout(LayoutKind.Sequential)] public struct MouseRecord { public ConsolePoint Position; public MouseButtonState ButtonState; public ControlKeyState ControlKeyState; public MouseEventFlagsState EventFlags; }

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)] public struct KeyboardRecord { [FieldOffset(0)] public bool KeyDown; [FieldOffset(4)] public ushort RepeatCount; [FieldOffset(6)] public ushort VirtualKeyCode; [FieldOffset(8)] public ushort VirtualScanCode; [FieldOffset(10)] public char UnicodeChar; [FieldOffset(10)] public byte AsciiChar; [FieldOffset(12)] public ControlKeyState ControlKeyState;

public ConsoleKey ConsoleKey => (ConsoleKey)VirtualKeyCode;

}

public struct WindowBufferSizeRecord { public ConsolePoint Size; }

Я не сам с нуля это писал, но существенно переписал то что нашел на просторах англоязычного StackOverflow.

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

ConsoleInputHandler.cs

public class ConsoleInputHandler : IDisposable
{
    private readonly CancellationTokenSource _cts;
public Task Task { get; private set; }

public delegate void ConsoleMouseEvent(MouseRecord r);
public delegate void ConsoleKeyEvent(KeyboardRecord r);
public delegate void ConsoleWindowBufferSizeEvent(WindowBufferSizeRecord r);

public event ConsoleMouseEvent MouseEvent;
public event ConsoleKeyEvent KeyEvent;
public event ConsoleWindowBufferSizeEvent WindowBufferSizeEvent;

public ConsoleInputHandler()
{
    _cts = new CancellationTokenSource();
    Run();
}

private void Run()
{
    IntPtr handle = NativeMethods.GetStdHandle(ConsoleInputHandle.StandardInput);
    InputRecord[] inputBuffer = new InputRecord[10];
    Task = Task.Run(() =>
    {
        CancellationToken token = _cts.Token;
        int numRead = 0;
        while (!token.IsCancellationRequested)
        {
            if (NativeMethods.ReadConsoleInput(handle, inputBuffer, inputBuffer.Length, ref numRead))
            {
                for (int i = 0; i < numRead; i++)
                {
                    switch (inputBuffer[i].EventType)
                    {
                        case ConsoleEventType.Mouse:
                            MouseEvent?.Invoke(inputBuffer[i].MouseEvent);
                            break;
                        case ConsoleEventType.Keyboard:
                            KeyEvent?.Invoke(inputBuffer[i].KeyEvent);
                            break;
                        case ConsoleEventType.WindowBufferSize:
                            WindowBufferSizeEvent?.Invoke(inputBuffer[i].WindowBufferSizeEvent);
                            break;
                        case ConsoleEventType.Menu:
                        case ConsoleEventType.Focus:
                            break;
                    }
                }
            }
            else
                break;
        }
    });
}

public void Stop()
{
    _cts.Cancel();
}

private bool disposed;

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    if (disposed)
        throw new ObjectDisposedException(nameof(ConsoleInputHandler));

    if (!_cts.IsCancellationRequested)
        _cts.Cancel();

    if (disposing)
    {
        Task.Wait();
        _cts.Dispose();
    }
    disposed = true;
}

~ConsoleInputHandler() => Dispose(false);

}

Вот, собственно и всё с хитрым кодом. Есть один нюанс, если вы запустите чтение ввода с помощью выше упомянутого класса с одновременным ожиданием Console.ReadKey(), то работать будет плохо, я сам долго не мог понять, почему не работает как надо, поэтому условие выхода из консольного приложения я унес в обработчик нажатий клавиш, а само ожидание сделал асинхронным "пока не остановится сам класс ConsoleInputHandler". Второй нюанс - чтобы работать с мышью в консоли, надо отрубить "быстрое выделение" этой мышью в консоли, этим занимается ниже приведенный метод SetupConsole().

Program.cs

class Program
{
    private static ConsoleInputHandler inputHandler;
static async Task Main(string[] args)
{
    SetupConsole();
    using (inputHandler = new ConsoleInputHandler())
    {
        inputHandler.MouseEvent += ConsoleInputHandler_MouseEvent;
        inputHandler.KeyEvent += ConsoleInputHandler_KeyEvent;
        await inputHandler.Task;
    }
    Console.WriteLine("Exited.");
    Console.ReadKey();
}

private static void ConsoleInputHandler_KeyEvent(KeyboardRecord r)
{
    if (r.KeyDown && r.ConsoleKey == ConsoleKey.Escape)
    {
        inputHandler.Stop();
    }
    else
    {
        int width = Console.BufferWidth - 1;
        Console.SetCursorPosition(0, 0);
        Console.WriteLine($"KeyDown: {r.KeyDown}".PadRight(width));
        Console.WriteLine($"KeyChar: {r.UnicodeChar}, ConsoleKey: {r.ConsoleKey}".PadRight(width));
        Console.WriteLine($"RepeatCount: {r.RepeatCount}".PadRight(width));
        Console.WriteLine($"Controls: {r.ControlKeyState}".PadRight(width));
    }
}

private static void ConsoleInputHandler_MouseEvent(MouseRecord r)
{
    int width = Console.BufferWidth - 1;
    Console.SetCursorPosition(0, 0);
    Console.WriteLine($"Position: {r.Position.X}, {r.Position.Y}".PadRight(width));
    Console.WriteLine($"Buttons: {r.ButtonState}".PadRight(width));
    Console.WriteLine($"Flags: {r.EventFlags}".PadRight(width));
    Console.WriteLine($"Controls: {r.ControlKeyState}".PadRight(width));
}

private static void SetupConsole()
{
    IntPtr handle = NativeMethods.GetStdHandle(ConsoleInputHandle.StandardInput);
    ConsoleMode mode = default;
    NativeMethods.GetConsoleMode(handle, ref mode);
    mode &= ~ConsoleMode.QuickEditMode;
    NativeMethods.SetConsoleMode(handle, mode);
}

}

Вывод в консоль будет такой, будет интерактивно показывать, куда едет мышь и что нажимается на клавиатуре.

Position: 39, 14
Buttons: LeftButton
Flags: Move
Controls: None
aepot
  • 49,560
  • 1
    Проблема в том, что _cts.Cancel() в ConsoleListener_KeyEvent пытается дождаться выполнения всех запланированных обратных вызовов на CancellationToken. Когда он используется с await Task.Delay, в качестве обратного вызова на нем запланирован хвост асинхронного метода. Если этот хвост вызывает Task.Wait, оба будут ждать вечно. Логичное решение - уйти в фоновый поток: Task.Run(() => _cts.Cancel());_cts.Token.WaitHandle.WaitOne();, но это можно реализовать проще, см. ответ. – MSDN.WhiteKnight Nov 09 '20 at 06:01
  • 1
    Вот более подробное описание, почему происходит дедлок: https://stackoverflow.com/a/31496425/8674428 – MSDN.WhiteKnight Nov 09 '20 at 06:08
  • @MSDN.WhiteKnight и правда теперь работает, вот ни за что бы не подумал про эти обратные вызовы, буду знать. Про переусложнение понял, чуть-чуть переписал в сторону упрощения, но асинхронность оставил. Спасибо большое! – aepot Nov 09 '20 at 09:42
4

Ответ aepot правильный, но он слишком переусложнен (в первой редакции) и это можно реализовать проще. Здесь асинхронный код вообще не нужен, без него проблемы дедлока не будет и можно дождаться завершения через WaitHandle.WaitOne:

public class ConsoleInputHandler : IDisposable
{
    public delegate void ConsoleMouseEvent(MouseRecord r);
    public delegate void ConsoleKeyEvent(KeyboardRecord r);
    public delegate void ConsoleWindowBufferSizeEvent(WindowBufferSizeRecord r);
public event ConsoleMouseEvent MouseEvent;

public event ConsoleKeyEvent KeyEvent;

public event ConsoleWindowBufferSizeEvent WindowBufferSizeEvent;

private readonly CancellationTokenSource _cts;

public ConsoleInputHandler()
{
    _cts = new CancellationTokenSource();
}

public void Run()
{
    IntPtr handle = NativeMethods.GetStdHandle(ConsoleInputHandle.StandardInput);
    InputRecord[] inputBuffer = new InputRecord[10];
    CancellationToken token = _cts.Token;
    int numRead = 0;
    while (!token.IsCancellationRequested)
    {
        if (NativeMethods.ReadConsoleInput(handle, inputBuffer, inputBuffer.Length, ref numRead))
        {
            for (int i = 0; i < numRead; i++)
            {
                switch (inputBuffer[i].EventType)
                {
                    case ConsoleEventType.Mouse:
                        MouseEvent?.Invoke(inputBuffer[i].MouseEvent);
                        break;
                    case ConsoleEventType.Keyboard:
                        KeyEvent?.Invoke(inputBuffer[i].KeyEvent);
                        break;
                    case ConsoleEventType.WindowBufferSize:
                        WindowBufferSizeEvent?.Invoke(inputBuffer[i].WindowBufferSizeEvent);
                        break;
                    case ConsoleEventType.Menu:
                    case ConsoleEventType.Focus:
                        break;
                }
            }
        }
        else
            break;
    }
}

public void Stop()
{
    if (disposed)
        throw new ObjectDisposedException(nameof(ConsoleInputHandler));

    if (!_cts.IsCancellationRequested) _cts.Cancel();

    _cts.Token.WaitHandle.WaitOne();
}

private bool disposed;

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    if (disposed)
        throw new ObjectDisposedException(nameof(ConsoleInputHandler));

    if (!_cts.IsCancellationRequested)
    {
        _cts.Cancel();
    }

    _cts.Token.WaitHandle.WaitOne();

    if (disposing)
    {
        _cts.Dispose();
    }
    disposed = true;
}

~ConsoleInputHandler() => Dispose(false);

}

class Program { static ConsoleInputHandler inputHandler;

static void Main(string[] args)
{
    SetupConsole();
    using (inputHandler = new ConsoleInputHandler())
    {
        inputHandler.MouseEvent += ConsoleListener_MouseEvent;
        inputHandler.KeyEvent += ConsoleListener_KeyEvent;
        inputHandler.Run();
    }
    Console.WriteLine("Exited.");
    Console.ReadKey();
}

private static void ConsoleListener_KeyEvent(KeyboardRecord r)
{
    if (r.KeyDown && r.VirtualKeyCode == (ushort)ConsoleKey.Escape)
    {
        inputHandler.Stop();
    }
    else
    {
        int width = Console.BufferWidth - 1;
        Console.SetCursorPosition(0, 0);
        Console.WriteLine($"KeyDown: {r.KeyDown}".PadRight(width));
        Console.WriteLine($"KeyChar: {r.UnicodeChar}, ConsoleKey: {(ConsoleKey)r.VirtualKeyCode}".PadRight(width));
        Console.WriteLine($"RepeatCount: {r.RepeatCount}".PadRight(width));
        Console.WriteLine($"Controls: {r.ControlKeyState}".PadRight(width));
    }
}

private static void SetupConsole()
{
    IntPtr handle = NativeMethods.GetStdHandle(ConsoleInputHandle.StandardInput);
    ConsoleMode mode = 0;
    NativeMethods.GetConsoleMode(handle, ref mode);
    mode &= ~ConsoleMode.QuickEditMode;
    NativeMethods.SetConsoleMode(handle, mode);
}

private static void ConsoleListener_MouseEvent(MouseRecord r)
{
    int width = Console.BufferWidth - 1;
    Console.SetCursorPosition(0, 0);
    Console.WriteLine($"Position: {r.Position.X}, {r.Position.Y}".PadRight(width));
    Console.WriteLine($"Buttons: {r.ButtonState}".PadRight(width));
    Console.WriteLine($"Flags: {r.EventFlags}".PadRight(width));
    Console.WriteLine($"Controls: {r.ControlKeyState}".PadRight(width));
}

}