Fundamentals

Інтерактивна Консоль (Classic)

Повний гайд по роботі з System.Console API в C# - від базового вводу-виводу до створення інтерактивних меню з навігацією курсором

Інтерактивна Консоль (Classic)

Вступ

Уявіть, що вам потрібно створити інструмент для системного адміністратора — консольну утиліту з кольоровим меню, можливістю навігації стрілками та індикатором прогресу. Або інтерактивний інсталятор, що виводить статус різними кольорами та дозволяє користувачу вибирати опції без повторного введення команд.

Все це можливо завдяки потужному API класу System.Console, який надає набагато більше можливостей, ніж просто Console.WriteLine(). У цьому розділі ми детально розглянемо всі аспекти роботи з консоллю: від базових методів вводу-виводу до створення складних інтерактивних інтерфейсів.

Передумови: Для комфортного вивчення матеріалу потрібно розуміти базові типи даних, змінні, методи та оператори C#.

System.Console API: Основи вводу-виводу

Write vs WriteLine

Метод Console.Write() виводить текст без переходу на новий рядок, тоді як Console.WriteLine() додає символ нового рядка після виводу.

МетодПоведінкаТипове використання
Write()Виводить текст без переходу на новий рядокВиведення тексту в один рядок, підказки для вводу
WriteLine()Виводить текст і переходить на новий рядокВиведення окремих повідомлень, логів
Console.Write("Завантаження");
Console.Write(".");
Console.Write(".");
Console.Write(".");
Console.WriteLine(" Готово!");

// Виведе: Завантаження... Готово!

ReadLine vs ReadKey

Існує два основні способи отримання вводу від користувача: ReadLine() для читання цілого рядка тексту та ReadKey() для читання одного натискання клавіші.

МетодПовертаєБлокує виконанняТипове використання
ReadLine()string?Так, до EnterВведення тексту, імен, значень
ReadKey()ConsoleKeyInfoТак, до натискання будь-якої клавішіМеню навігації, підтвердження (Y/N)
ReadKey(true)ConsoleKeyInfoТак, але не відображає символВведення паролів, керування UI
Console.Write("Введіть ваш email: ");
string? email = Console.ReadLine();

if (string.IsNullOrWhiteSpace(email))
{
    Console.WriteLine("Email не може бути порожнім!");
    return;
}

Console.WriteLine($"Email збережено: {email}");
Best Practice: Використовуйте ReadKey(true) для створення інтерактивних меню, щоб символи не відображалися на екрані. Це робить інтерфейс чистішим та професійнішим.

Кастомізація Консолі

Кольори (Colors)

Консоль дозволяє налаштовувати колір тексту (Foreground) та фону (Background) за допомогою перерахування ConsoleColor.

Встановлення кольорів

// Зберігаємо поточні кольори
ConsoleColor originalFg = Console.ForegroundColor;
ConsoleColor originalBg = Console.BackgroundColor;

// Встановлюємо нові кольори
Console.ForegroundColor = ConsoleColor.Green;
Console.BackgroundColor = ConsoleColor.DarkGray;
Console.WriteLine("Цей текст зелений на темно-сірому тлі");

// Відновлюємо оригінальні кольори
Console.ForegroundColor = originalFg;
Console.BackgroundColor = originalBg;

Використання ResetColor()

Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Блакитний текст");

Console.ResetColor(); // Повертає до стандартних кольорів
Console.WriteLine("Стандартний текст");

Доступні кольори ConsoleColor

КолірОписРекомендоване використання
Black, DarkGray, Gray, WhiteВідтінки сірогоФон, звичайний текст
Red, DarkRedЧервонийПомилки, критичні попередження
Green, DarkGreenЗеленийУспішні операції, підтвердження
Yellow, DarkYellowЖовтийПопередження, важлива інформація
Blue, DarkBlueСинійІнформаційні повідомлення
Cyan, DarkCyanБлакитнийПідказки, допоміжна інформація
Magenta, DarkMagentaПурпурнийЗаголовки, акценти
void WriteSuccess(string message)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine($"✓ {message}");
    Console.ResetColor();
}

void WriteError(string message)
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine($"✗ {message}");
    Console.ResetColor();
}

void WriteWarning(string message)
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"⚠ {message}");
    Console.ResetColor();
}

// Використання
WriteSuccess("Файл успішно збережено");
WriteError("Не вдалося підключитися до бази даних");
WriteWarning("Залишилось мало дискового простору");
Обмеження платформ: На деяких Unix/Linux терміналах підтримка кольорів може бути обмеженою. Перед використанням кольорів у production-застосунках, перевірте підтримку через властивість Console.IsOutputRedirected.

Інші параметри кастомізації

// Встановлення заголовку вікна консолі
Console.Title = "Мій Консольний Застосунок v1.0";

// Динамічна зміна заголовку (наприклад, для індикатора прогресу)
for (int i = 0; i <= 100; i += 10)
{
    Console.Title = $"Завантаження... {i}%";
    Thread.Sleep(500);
}

Console.Title = "Завантаження завершено!";
Console.Beep на різних платформах:
  • Windows: Повністю підтримується з можливістю налаштування частоти (37-32767 Hz) та тривалості.
  • Linux/macOS: Може не підтримуватися або виконувати стандартний системний звук незалежно від параметрів.

Керування Курсором

Керування позицією курсору дозволяє створювати динамічні інтерфейси, оновлювати текст на місці та будувати інтерактивні меню без прокрутки екрану.

Основні методи роботи з курсором

Метод/ВластивістьОписПриклад значення
SetCursorPosition(int left, int top)Встановлює позицію курсоруSetCursorPosition(0, 10)
GetCursorPosition()Повертає поточну позицію (Left, Top)(5, 12)
CursorLeftКоордината X (колонка)15
CursorTopКоордината Y (рядок)8
CursorVisibleВидимість курсору (true/false)Console.CursorVisible = false;
Console.Clear();

// Виведення в конкретну позицію
Console.SetCursorPosition(10, 5);
Console.Write("Текст у позиції (10, 5)");

// Отримання поточної позиції (tuple deconstruction)
var (x, y) = Console.GetCursorPosition();
Console.SetCursorPosition(0, 10);
Console.WriteLine($"Попередня позиція: X={x}, Y={y}");

// Переміщення курсору на початок рядка
Console.CursorLeft = 0;
Console.Write("Початок рядка");

Практичний приклад: Інтерактивне меню з навігацією

Створюємо структуру меню

class MenuItem
{
    public string Text { get; set; } = "";
    public Action Action { get; set; } = () => { };
}

MenuItem[] menuItems = new[]
{
    new MenuItem { Text = "Створити новий файл", Action = () => Console.WriteLine("\nСтворення файлу...") },
    new MenuItem { Text = "Відкрити файл", Action = () => Console.WriteLine("\nВідкриття файлу...") },
    new MenuItem { Text = "Налаштування", Action = () => Console.WriteLine("\nНалаштування...") },
    new MenuItem { Text = "Вихід", Action = () => Environment.Exit(0) }
};

Реалізуємо логіку навігації

int selectedIndex = 0;
Console.CursorVisible = false;

try
{
    while (true)
    {
        Console.Clear();
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine("╔═══════════════════════════╗");
        Console.WriteLine("║      ГОЛОВНЕ МЕНЮ         ║");
        Console.WriteLine("╚═══════════════════════════╝");
        Console.ResetColor();
        Console.WriteLine("\nВикористовуйте ↑↓ для навігації, Enter для вибору\n");

        // Малюємо меню
        for (int i = 0; i < menuItems.Length; i++)
        {
            if (i == selectedIndex)
            {
                Console.BackgroundColor = ConsoleColor.DarkGray;
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine($"  ► {menuItems[i].Text}  ");
                Console.ResetColor();
            }
            else
            {
                Console.WriteLine($"    {menuItems[i].Text}");
            }
        }

        // Обробка клавіш
        ConsoleKeyInfo key = Console.ReadKey(true);

        switch (key.Key)
        {
            case ConsoleKey.UpArrow:
                selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : menuItems.Length - 1;
                break;
            case ConsoleKey.DownArrow:
                selectedIndex = selectedIndex < menuItems.Length - 1 ? selectedIndex + 1 : 0;
                break;
            case ConsoleKey.Enter:
                Console.Clear();
                menuItems[selectedIndex].Action();
                Console.WriteLine("\nНатисніть будь-яку клавішу для повернення в меню...");
                Console.ReadKey(true);
                break;
        }
    }
}
finally
{
    Console.CursorVisible = true;
}

Професійний UX: Завжди приховуйте курсор (CursorVisible = false) при створенні інтерактивних меню, щоб уникнути мигаючого курсору, який псує візуальний вигляд.

Буфери та Вікна

Консоль має дві важливі концепції: буфер (buffer) та вікно (window). Розуміння їх різниці критично важливе для створення професійних консольних інтерфейсів.

Loading diagram...
graph TD
    A[Console Buffer] -->|Містить весь текст| B[Console Window]
    B -->|Видима частина буферу| C[Екран користувача]

    style A fill:#64748b,stroke:#334155,color:#ffffff
    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff

BufferSize vs WindowSize

ПараметрОписНалаштування
Buffer (Буфер)Загальний розмір області для тексту. Може бути більшим за вікно.Console.BufferHeight, Console.BufferWidth, Console.SetBufferSize()
Window (Вікно)Видима частина буферу на екрані. Завжди ≤ буферу.Console.WindowHeight, Console.WindowWidth, Console.SetWindowSize()
Важливо: Розмір вікна не може перевищувати розмір буферу. Якщо буфер більший за вікно, з'являється смуга прокрутки (scrollbar).

// Отримання поточних розмірів
int bufferWidth = Console.BufferWidth;
int bufferHeight = Console.BufferHeight;

int windowWidth = Console.WindowWidth;
int windowHeight = Console.WindowHeight;

Console.WriteLine($"Буфер: {bufferWidth}x{bufferHeight}");
Console.WriteLine($"Вікно: {windowWidth}x{windowHeight}");

// Максимально можливий розмір вікна
int maxWidth = Console.LargestWindowWidth;
int maxHeight = Console.LargestWindowHeight;
Console.WriteLine($"Максимум: {maxWidth}x{maxHeight}");
Platform-specific: На Linux та macOS можливості зміни розміру буферу та вікна можуть бути обмеженими або недоступними. Завжди обгортайте виклики SetBufferSize та SetWindowSize у try-catch.

Перенаправлення Потоків (Stream Redirection)

Консольний застосунок працює з трьома стандартними потоками: StandardInput (введення), StandardOutput (виведення) та StandardError (помилки). Розуміння цих потоків відкриває великі можливості для інтеграції з іншими програмами та автоматизації.

Три стандартні потоки

Loading diagram...
graph LR
    A[Користувач/Інший процес] -->|Введення| B[StandardInput]
    C[Програма] -->|Нормальне виведення| D[StandardOutput]
    C -->|Помилки/Діагностика| E[StandardError]

    B --> C
    D --> F[Консоль/Файл/Інший процес]
    E --> F

    style A fill:#64748b,stroke:#334155,color:#ffffff
    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E fill:#f59e0b,stroke:#b45309,color:#ffffff
    style F fill:#64748b,stroke:#334155,color:#ffffff
ПотікВластивістьПризначенняЗа замовчуванням
Стандартний ввідConsole.InЧитання даних від користувачаКлавіатура
Стандартний вивідConsole.OutВиведення результатів роботиКонсольне вікно
Стандартний потік помилокConsole.ErrorВиведення помилок та діагностикиКонсольне вікно

Pipes та перенаправлення в терміналі

# Перенаправлення виводу у файл
myapp.exe > output.txt

# Перенаправлення помилок у файл
myapp.exe 2> errors.txt

# Перенаправлення і виводу, і помилок
myapp.exe > output.txt 2>&1

# Pipe: передача виводу одної програми до іншої
myapp.exe | findstr "ERROR"

# Читання з файлу як введення
myapp.exe < input.txt

Програмне перенаправлення потоків

using System;
using System.IO;

// Зберігаємо оригінальні потоки
TextWriter originalOut = Console.Out;
TextWriter originalError = Console.Error;

try
{
    // Перенаправляємо вивід у файл
    using (StreamWriter fileOut = new StreamWriter("output.log"))
    using (StreamWriter fileErr = new StreamWriter("errors.log"))
    {
        Console.SetOut(fileOut);
        Console.SetError(fileErr);

        Console.WriteLine("Це повідомлення йде у output.log");
        Console.Error.WriteLine("Це помилка йде у errors.log");

        // Виконуємо деяку роботу
        PerformWork();
    }
}
finally
{
    // Відновлюємо оригінальні потоки
    Console.SetOut(originalOut);
    Console.SetError(originalError);

    Console.WriteLine("Логування завершено. Перевірте output.log та errors.log");
}

void PerformWork()
{
    Console.WriteLine("Початок обробки...");
    try
    {
        // Симуляція роботи
        throw new InvalidOperationException("Тестова помилка");
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine($"ПОМИЛКА: {ex.Message}");
    }
    Console.WriteLine("Обробка завершена.");
}
Тестування консольних застосунків: Використовуйте StringWriter та StringReader для автоматизованого тестування консольних програм без реального вводу-виводу. Це значно спрощує написання unit-тестів.

Визначення перенаправлення

// Перевірка, чи потоки перенаправлені
bool isInputRedirected = Console.IsInputRedirected;
bool isOutputRedirected = Console.IsOutputRedirected;
bool isErrorRedirected = Console.IsErrorRedirected;

Console.WriteLine($"Ввід перенаправлений: {isInputRedirected}");
Console.WriteLine($"Вивід перенаправлений: {isOutputRedirected}");
Console.WriteLine($"Помилки перенаправлені: {isErrorRedirected}");

// Адаптація поведінки
if (!Console.IsOutputRedirected)
{
    // Кольори працюють лише в справжній консолі
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Кольоровий вивід доступний");
    Console.ResetColor();
}
else
{
    // При перенаправленні використовуємо простий текст
    Console.WriteLine("Вивід перенаправлено, кольори відключені");
}

Troubleshooting: Типові проблеми

Проблема: Некоректне відображення Unicode символів

Симптоми: Кирилиця або спецсимволи відображаються як ? або квадратики.

Рішення:

// Встановлюємо UTF-8 кодування для виводу
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.InputEncoding = System.Text.Encoding.UTF8;

Console.WriteLine("Тепер українська мова відображається коректно! ✓");
Console.WriteLine("Емодзі теж працюють: 🎉 🚀 ⭐");

Проблема: SetCursorPosition викидає виняток

Симптоми: ArgumentOutOfRangeException при спробі встановити позицію курсору.

Рішення:

void SafeSetCursorPosition(int left, int top)
{
    // Перевіряємо межі
    if (left < 0 || left >= Console.BufferWidth)
        return;

    if (top < 0 || top >= Console.BufferHeight)
        return;

    try
    {
        Console.SetCursorPosition(left, top);
    }
    catch (ArgumentOutOfRangeException)
    {
        // Ігноруємо, якщо розміри змінилися під час виконання
    }
}

Проблема: Кольори не працюють в CI/CD або при перенаправленні

Рішення:

void WriteColored(string text, ConsoleColor color)
{
    if (Console.IsOutputRedirected)
    {
        // При перенаправленні виводимо без кольорів
        Console.WriteLine(text);
    }
    else
    {
        Console.ForegroundColor = color;
        Console.WriteLine(text);
        Console.ResetColor();
    }
}

Проблема: Console.ReadKey не працює на Linux в певних терміналах

Рішення: Використовуйте бібліотеку System.Console обережно та додайте fallback:

try
{
    ConsoleKeyInfo key = Console.ReadKey(true);
    // Обробка клавіші
}
catch (InvalidOperationException)
{
    // Fallback для середовищ без підтримки ReadKey
    Console.WriteLine("Натисніть Enter для продовження...");
    Console.ReadLine();
}

Практичні Завдання

Рівень 1: Базове кольорове меню

Мета: Відпрацювати роботу з кольорами та базовим ReadKey.

Завдання: Створіть програму з меню, яке:

  • Виводить 3-4 пункти різними кольорами
  • Чекає на натискання клавіші (1, 2, 3 або Q для виходу)
  • Виводить повідомлення про вибраний пункт

Рівень 2: Інтерактивний список з навігацією

Мета: Відпрацювати керування курсором та створення динамічного UI.

Завдання: Створіть програму зі списком елементів (масив рядків), яка:

  • Дозволяє переміщуватися по списку стрілками ↑↓
  • Підсвічує поточний елемент іншим кольором/фоном
  • Вибирає елемент по Enter і виводить повідомлення
  • Виходить по Escape

Рівень 3: Консольний текстовий редактор з прокруткою

Мета: Освоїти роботу з BufferSize, ScrollBars та складну навігацію.

Завдання: Створіть простий текстовий редактор, який:

  • Дозволяє вводити багато рядків тексту (зберігаються у List<string>)
  • Показує рядки у вікні фіксованого розміру (наприклад, 10 рядків)
  • Дозволяє прокручувати вміст стрілками, якщо рядків більше, ніж поміщається на екрані
  • Показує індикатор поточного рядка
  • Зберігає текст у файл по Ctrl+S
  • Виходить по Escape
Розширення: Додайте можливість редагування тексту в рядках, видалення рядків (Delete/Backspace), пошук (Ctrl+F) та горизонтальну прокрутку для довгих рядків.

Підсумок

У цьому розділі ми детально розглянули можливості System.Console API:

  • Базові методи: Write/WriteLine для виводу, ReadLine/ReadKey для вводу
  • Кастомізація: Робота з кольорами (ForegroundColor, BackgroundColor), заголовками та звуками
  • Керування курсором: Позиціонування (SetCursorPosition), приховування та отримання координат
  • Буфери та вікна: Розуміння різниці між BufferSize та WindowSize для створення професійних інтерфейсів
  • Потоки: Робота з StandardInput, StandardOutput, StandardError та їх перенаправлення

Ці знання дозволяють створювати інтерактивні консольні застосунки з зручним UX: від простих меню до складних текстових редакторів та інструментів моніторингу.

Наступні кроки: Для створення ще більш потужних консольних інтерфейсів дослідіть бібліотеки як Spectre.Console, Terminal.Gui або Colorful.Console, які надають готові компоненти: таблиці, дерева, progress bars та багато іншого.