С# одновременный ввод и вывод консоли?

Я пишу серверное приложение и хочу, чтобы оно было консольным. Мне нужно, чтобы пользователь мог вводить разные команды, но при этом есть вероятность, что что-то будет выводиться в консоль, пока пользователь пишет. Это портит буфер. Есть ли чистый способ сделать это?

Спасибо.


person Alex Turpin    schedule 11.05.2009    source источник
comment
Я не знаю ни одного простого способа, нет. Я видел, как некоторые ужасно зрелые продукты (gdb, windbg) не беспокоились об этом, хотя обычно они не выводят асинхронную информацию на экран. Какого рода взаимодействие вы ищете? Вы выяснили, как, по вашему мнению, должна вести себя консоль?   -  person Greg D    schedule 12.05.2009
comment
На самом деле это не сложная концепция, все, что я хочу, это чтобы пользователь мог вводить текст, а подсказка была после последней написанной строки. Если во время ввода пользователем пишется какая-либо другая строка, подсказка перерисовывается после этой строки.   -  person Alex Turpin    schedule 12.05.2009


Ответы (7)


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

  static int outCol, outRow, outHeight = 10;

  static void Main(string[] args)
  {
     bool quit = false;
     System.DateTime dt = DateTime.Now;
     do
     {
        if (Console.KeyAvailable)
        {
           if (Console.ReadKey(false).Key == ConsoleKey.Escape)
              quit = true;
        }
        System.Threading.Thread.Sleep(0);
        if (DateTime.Now.Subtract(dt).TotalSeconds > .1)
        {
           dt = DateTime.Now;
           WriteOut(dt.ToString(" ss.ff"), false);
        }            
     } while (!quit);
  }

  static void WriteOut(string msg, bool appendNewLine)
  {
     int inCol, inRow;
     inCol = Console.CursorLeft;
     inRow = Console.CursorTop;

     int outLines = getMsgRowCount(outCol, msg) + (appendNewLine?1:0);
     int outBottom = outRow + outLines;
     if (outBottom > outHeight)
        outBottom = outHeight;
     if (inRow <= outBottom)
     {
        int scrollCount = outBottom - inRow + 1;
        Console.MoveBufferArea(0, inRow, Console.BufferWidth, 1, 0, inRow + scrollCount);
        inRow += scrollCount;
     }
     if (outRow + outLines > outHeight)
     {
        int scrollCount = outRow + outLines - outHeight;
        Console.MoveBufferArea(0, scrollCount, Console.BufferWidth, outHeight - scrollCount, 0, 0);
        outRow -= scrollCount;
        Console.SetCursorPosition(outCol, outRow);
     }
     Console.SetCursorPosition(outCol, outRow);
     if (appendNewLine)
        Console.WriteLine(msg);
     else
        Console.Write(msg);
     outCol = Console.CursorLeft;
     outRow = Console.CursorTop;
     Console.SetCursorPosition(inCol, inRow);
  }

  static int getMsgRowCount(int startCol, string msg)
  {
     string[] lines = msg.Split('\n');
     int result = 0;
     foreach (string line in lines)
     {
        result += (startCol + line.Length) / Console.BufferWidth;
        startCol = 0;
     }
     return result + lines.Length - 1;
  }
person BlueMonkMN    schedule 11.05.2009

Лично я бы использовал обработчики событий для управления консолью, которая одновременно обрабатывает как ввод, так и вывод, создал класс ScreenManager или что-то еще, внутри этого класса добавил метод void RunProgram(), создал событие с обработчиком и необходимыми переменными для чтения ключ ввода "Console.ReadKey(bool).key".

static Consolekey newKey;

в вашей основной программе создайте экземпляр вашего класса «как вы его назвали», затем создайте поток внутреннего метода этого экземпляра, Thread coreThread = new Thread(delegate() {myinstance.myProgramMrthod()});

loop в вашем main, пока потоки не запустятся и не запустятся. пока (!Thread.IsAlive) ;

затем создайте основной цикл программы.

while (true)
{
}

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

customThread.Join();

теперь у вас есть два потока, работающих отдельно.

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

switch (newkey)
{
case Consolekey.enter
Console.WriteLine("enter pressed");
break;

ect, ect.

default:
Console.write(newkey); // writes character key that dont match above conditions to the screen. 
break;
}

придерживайтесь здесь своей логики с тем, как вы хотите обращаться с ключами. Как использовать несколько клавиш-модификаторов в C# может быть какой-то помощи.

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

while (true)
{
newKey = Console.ReadKey(true).Key;
if (newKey != oldKey)
{
KeyChange.Invoke();
}
}

этот цикл сохраняет любую нажатую клавишу, а затем проверяет, есть ли новая клавиша, если true запускает событие.

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

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

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

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

создать нового делегата с подписью со строковым аргументом, недействительным делегатом myDelegate(string Arg). затем создайте событие с этим делегатом, назовите его новой строкой, новым вводом, как хотите.

обработчик событий примет строковый аргумент (представляющий текст обновления консоли: то, что вы хотите вставить в консоль над пользовательским вводом), он захватит текст, который пользователь вводил в консоль, сохранит его, а затем распечатает параметр string на консоль, затем распечатайте введенные пользователем данные.

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

вызовите строку «ввод» или что-то еще.

в область по умолчанию дескриптора события смены клавиши добавьте ввод +=newkey.

в консоли секции Consolekey.enter введите строку ввода, затем введите = string.empty Или string = "".

в обработчике событий добавить немного логики.

public void OnInsert(string Argument)
        {
            Console.CursorTop -= 1;

    // moves the cursor to far left so new input overwrites the old.



    // if arg string is longer, then print arg string then print input  // string.

            if (Argument.Length > input.Length)
            {
                Console.WriteLine(Argument);
                Console.WriteLine(input);
            }
            else
            {

    // if the users input if longer than the argument text then print
    // out the argument text, then print white spaces to overwrite the
    // remaining input characters still displayed on screen.


                for (int i = 0; i < input.Length;i++ )
                {
                    if (i < Argument.Length)
                    {
                        Console.Write(Argument[i]);
                    }
                    else
                    {
                        Console.Write(' ');
                    }
                }
                Console.Write(Environment.NewLine);
                Console.WriteLine(input);
            }
        }

hope this helps some of you, its not perfect, a quick put together test that works enough to be built on.
person Gorlykio    schedule 22.12.2012

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

person Lucas B    schedule 11.05.2009
comment
+1. В любом случае, как бедный пользователь сможет определить разницу между выводом, вызванным вводом текста, и выводом, вызванным каким-то дурацким фоновым процессом, который запустился сразу после того, как он нажал Enter? - person MarkJ; 06.09.2012

Такого рода вещи становятся несколько более простой проблемой, если вы рассматриваете сервер как клиент-серверное приложение. Пусть сервер имеет «n» подключений к клиентским приложениям администратора, которые отправляют команды и получают выходные данные. Клиентское приложение может полностью разделить ввод и вывод, имея один поток для обработки ввода и один для обработки вывода.

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

person John Saunders    schedule 11.05.2009

Мой пример работает с использованием Console.MoveBufferArea(), но обратите внимание, что это не будет работать на платформах, отличных от Windows, потому что метод не реализовано на этих платформах.

В этом примере вы должны использовать Read() вместо Console.ReadLine() и Log(...) вместо Console.WriteLine(...) в своем коде.

class Program
{
    static void Main(string[] args)
    {
        // Reader
        new Thread(() =>
        {
            string line;
            while ((line = Read()) != null)
            {
                //...
            }
            Environment.Exit(0);
        }).Start();


        // Writer
        new Thread(() =>
        {
            while (true)
            {
                Thread.Sleep(1000);
                Log("----------");
            }
        }).Start();

    }

    static int lastWriteCursorTop = 0;
    static void Log(string message)
    {
        int messageLines = message.Length / Console.BufferWidth + 1;
        int inputBufferLines = Console.CursorTop - lastWriteCursorTop + 1;

        Console.MoveBufferArea(sourceLeft: 0, sourceTop: lastWriteCursorTop,
                               targetLeft: 0, targetTop: lastWriteCursorTop + messageLines,
                               sourceWidth: Console.BufferWidth, sourceHeight: inputBufferLines);

        int cursorLeft = Console.CursorLeft;
        Console.CursorLeft = 0;
        Console.CursorTop -= inputBufferLines - 1;
        Console.WriteLine(message);
        lastWriteCursorTop = Console.CursorTop;
        Console.CursorLeft = cursorLeft;
        Console.CursorTop += inputBufferLines - 1;
    }
    static string Read()
    {
        Console.Write(">"); // optional
        string line = Console.ReadLine();
        lastWriteCursorTop = Console.CursorTop;
        return line;
    }
}
person Tera    schedule 12.01.2019

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

person Richard Szalay    schedule 11.05.2009
comment
Я только что попробовал, но боюсь, я не совсем уверен, как это сделать... Я только что попробовал, и он выдал исключение. Поток не поддерживает поиск... - person Alex Turpin; 12.05.2009

Думаю, идеального способа добиться этого не существует. Что делает telnet (по крайней мере, последняя версия, которую я использовал), так это не печатает никаких входных данных (просто читает нажатия клавиш), а просто печатает выходные данные по мере их поступления. В качестве альтернативы можно хранить любые данные, которые необходимо вывести на консоль, в буфере и распечатывать их только после того, как пользователь завершит ввод своей команды. (Вы даже можете поставить временную метку на вывод, чтобы сделать его более очевидным.) Я действительно не вижу здесь лучшей альтернативы - вы неизбежно столкнетесь с проблемами, используя интерфейс синхронного ввода-вывода (т. е. командную строку) вместе с асинхронные операции в бэкенде.

person Noldorin    schedule 11.05.2009