Как работать с файлами

Возможность записывать/читать/изменять данные является одним из основополагающих процессов в работе компьютера. Грубо говоря больше то компьютер ничего и не умеет. Он только все время что-то меняет. А устройства на это реагируют и показывают картинку в случае монитора, издают звук если это колонка и т.д.

Нам будет интересовать работа с постоянной памятью. А именно, с файликами. Для начала мы ограничимся работой с текстовыми файлами.

Постановка задачи

Задача: посчитать сумму введенных пользователем чисел больших заданного числа T.

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

3
1 2 3 4 5 6 

То есть для такой записи у нас получается индивидуальная задача вида:

  • Среди чисел {1, 2, 3, 4, 5, 6} выбрать те, которые больше 3 и подсчитать их сумму.

Добавляем интерфейс

Не будем мудрить и для начала просто создадим форму с одной кнопкой (я изменил свойство Text кнопки, чтобы на ней была осознанная надпись):

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

Добавляем файл в проект

Для тестовых целей создадим файлик, кликнем правой кнопкой на название проекта и выберем Добавить/Создать элемент

в списке найдем текстовый файл и дадим ему название task.txt

файлик должен появится в обозревателе решений:

кликнем на этот файлик два раза, чтобы отредактировать его и добавим в него наши несчастные две строчки:

3
1 2 3 4 5 6 

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

Теперь переключимся на код формы (два раза кликнем на файл Form1.cs, затем нажмем F7). И приступим к редактированию обработчика события клика.

Читаем строчки из файла

Правим код:

using System;
// ...
using System.IO; // <<< добавил новый using, чтобы работать с файлами

namespace WindowsFormsApp5
{
    public partial class Form1 : Form
    {
        // ...

        private void button1_Click(object sender, EventArgs e)
        {
            // считываем содержимое файла как набор строк
            var lines = File.ReadAllLines("task.txt");
            
            // выведем содержимое на экран
            MessageBox.Show(lines[0] + "\n" + lines[1]);
        }
    }
}

запускаем и проверяем:

Выводим данные на форму

Данные читать умеем теперь давайте выведем их на форму. Я добавлю

  • два TextBox, один под строку с числами (имя: txtNumbers), второй под число T (имя: txtT);
  • два Label для подписей для текстбоксов;
  • кнопку c надписью Решить Задачу, на будущее.

теперь давайте сделаем чтобы по нажатию кнопки Считать данные, вместо MessageBox данные записывались в соответствующие поля на форме. Правим обработчик:

private void button1_Click(object sender, EventArgs e)
{
    // считываем содержимое файла как набор строк
    var lines = File.ReadAllLines("task.txt");

    // заполняем значения полей
    txtT.Text = lines[0];
    txtNumbers.Text = lines[1];
}

запускаем, проверяем:

Ура! Мы свели задаче к лабе 1, только заполнение у нас теперь автоматическое. Давайте теперь добавим возможность выбрать произвольный файл, и вывести его содержимое на форму.

Добавляем диалоговое окно выбора файла

Правим обработчик:

private void button1_Click(object sender, EventArgs e)
{
    var openFileDialog = new OpenFileDialog(); // создаем экземпляр диалогового окна

    // если пользователь не нажал OK, то выходим
    if (openFileDialog.ShowDialog() != DialogResult.OK)
        return; 

    // считываем содержимое файла как набор строк
    var lines = File.ReadAllLines(openFileDialog.FileName); // открываем файл выбранный в диалоговом окне

    // заполняем значения полей
    txtT.Text = lines[0];
    txtNumbers.Text = lines[1];
}

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

Папку по умолчанию можно поменять, например, на путь к exe файлу программы:

private void button1_Click(object sender, EventArgs e)
{
    var openFileDialog = new OpenFileDialog(); // создаем экземпляр диалогового окна
    openFileDialog.InitialDirectory = AppDomain.CurrentDomain.BaseDirectory;

    //...
}

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

Вернемся собственно к задаче.

Добавляю логику

Внедряем класс Logic, как и раньше, класс Form1 не трогаем, а после него добавляем класс Logic:

namespace WindowsFormsApp5
{
    // это не трогаем
    public partial class Form1 : Form
    {
       // ...
    }
    
    // класс с логикой
    public class Logic
    {
        /**
         * одна функция, которая принимает на вход массив и число
         * а выдает число
         */
        public static int Compute(int[] array, int T)
        {
            int sum = 0;
            foreach(var el in array)
            {
                if (el > T)
                {
                    sum += el;
                }
            }
            return sum;
        }
    }
}

Добавим обработчик для решения задачи

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

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var openFileDialog = new OpenFileDialog();
        // ...
        txtNumbers.Text = lines[1];
    }

    private void button2_Click(object sender, EventArgs e)
    {
        // ЭТО НОВЫЙ ОБРАБОТЧИК, дальше будем работать с ним
    }
}

Конвертируем данные

И так, у нас есть данные на форме, теперь надо их передать нашей функции в логике.

Давайте думать в чем у нас проблема. Мы знаем, что в txtNumbers у нас массив чисел, а в txtT число T, но мы не можем написать наш обработчик как

public partial class Form1 : Form
{
    // ...

    private void button2_Click(object sender, EventArgs e)
    {
        int sum = Logic.Compute(txtNumbers.Text, txtT.Text);

        // выведем содержимое на экран
        MessageBox.Show("Сумма чисел равна: " + sum.ToString());
    }
}

такой код даже не скомпилируется, ведь txtNumbers.Text и txtT.Text просто возвращает текст. Следовательно, нам надо этот текст преобразовать. Преобразовать значение txtT в число это как раз чихнуть, мы делали это уже много раз, достаточно написать:

var T = int.Parse(txtT.Text);

а вот как нам разбить значение txtNumbers.Text на набор чисел? Тут нам пригодится метод Split, который позволяет разбить строку с помощью указанного разделителя (в нашем случае это будет пробел)

Преобразуем строку в массив чисел

Есть как минимум два способа сделать, первый весьма громоздкий:

// разобьем строку пробелами на элементы, параметры несут следующий смысл: 
// new[] { " " } -- это список разделителей, то бишь только пробел
// StringSplitOptions.RemoveEmptyEntries -- означает что мы просим убирать пустые элементы, 
//      например, если рядом стоят два пробела, то технически между ними пустая строка
//      чтобы такие пустых строк не появлялось, добавляется этот параметр.
var numbersAsStrings = txtNumbers.Text.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);

// заведем список, куда будем добавлять преобразованные в числа элементы из разбитой строки
var numbers = new List<int>(); 

// в цикле каждый элемент разбитой строки, конвертируем в число и добавляем в массив numbers 
foreach(var number in numbersAsStrings)
{
    numbers.Add(int.Parse(number));
}

// преобразуем список в массив, чтобы можно было передать результат в метод Compute
var array = numbers.ToArray(); 

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

// первая строка такая же
var numbersAsStrings = txtNumbers.Text.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
// а тут преобразование на лету, с использованием анонимной лямбда-функции: x => int.Parse(x)
var array = numbersAsStrings.Select(x => int.Parse(x)).ToArray();

И так, получим такой обработчик:

private void button2_Click(object sender, EventArgs e)
{
    var T = int.Parse(txtT.Text);

    var numbersAsStrings = txtNumbers.Text.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
    var array = numbersAsStrings.Select(x => int.Parse(x)).ToArray();

    int sum = Logic.Compute(array, T);

    // выведем содержимое на экран
    MessageBox.Show("Сумма чисел равна: " + sum.ToString());
}

навешаем обработку ошибок, чтобы не падал на пустых TextBox`ах:

private void button2_Click(object sender, EventArgs e)
{
    int T;
    int[] array;

    try
    {
        T = int.Parse(txtT.Text);

        var numbersAsStrings = txtNumbers.Text.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
        array = numbersAsStrings.Select(x => int.Parse(x)).ToArray();
    } catch (FormatException)
    {
        return;
    }

    int sum = Logic.Compute(array, T);

    // выведем содержимое на экран
    MessageBox.Show("Сумма чисел равна: " + sum.ToString());
}

Выводим результат на форму

Для вывода на форму, добавим:

  • новое поле типа RichTextBox, установим его свойству (Name) значение txtAnswer
  • новую кнопку со свойством Text равным Сохранить в файл.

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

private void button2_Click(object sender, EventArgs e)
{
    int T;
    // ...
    int sum = Logic.Compute(array, T);

    // MessageBox.Show("Сумма чисел равна: " + sum.ToString()); << эту строчку убираем:
    
    // чтобы была возможность использовать форматирование прям для строки
    // создадим экземпляр специального класса StringBuilder
    var answerString = new StringBuilder();
    
    // добавим строку с форматированием
    answerString.AppendFormat("Сумма чисел больших {0} равна {1}", T, sum);
    
    // запишем результат нашей строки в новодобавленное поле, 
    // тут обязательно вызвать метод ToString
    txtAnswer.Text = answerString.ToString();
}

Сохраняем результат в файл

Переключимся на форму (кликнув два раза на Form1.cs в обозревателе решений). Затем кликнем два раза на кнопку Сохранить в файл чтобы добавить ей обработчик события. Окажемся в редакторе кода:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var openFileDialog = new OpenFileDialog();
        // ...
        txtNumbers.Text = lines[1];
    }

    private void button2_Click(object sender, EventArgs e)
    {
        int T;
        // ...
        txtAnswer.Text = answerString.ToString();
    }

    private void button3_Click(object sender, EventArgs e)
    {
        // НОВЫЙ ОБРАБОТЧИК, дальше работаем здесь
    }
}

записывать в файл не сильно сложнее чем читать из него, достаточно сделать следующее:

public partial class Form1 : Form
{
    // ...
    
    private void button3_Click(object sender, EventArgs e)
    {
        // первый параметр: в какой файл записывать,
        // второй параметр: что именно записывать
        File.WriteAllText("answer.txt", txtAnswer.Text);
    }
}

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

public partial class Form1 : Form
{
    // ...
    
    private void button3_Click(object sender, EventArgs e)
    {
        var openFileDialog = new OpenFileDialog(); // создаем экземпляр диалогового окна

        // если пользователь не нажал OK, то выходим
        if (openFileDialog.ShowDialog() != DialogResult.OK)
            return; 
    
        File.WriteAllText(openFileDialog.FileName, txtAnswer.Text);
    }
}

либо можно прям этот файл открыть:

public partial class Form1 : Form
{
    // ...
    
    private void button3_Click(object sender, EventArgs e)
    {
        File.WriteAllText("answer.txt", txtAnswer.Text);
        
        // открываем файл приложением по умолчанию
        System.Diagnostics.Process.Start("answer.txt");
    }
}

Вот собственно все так просто.

Как преобразовать текст в матрицу (добавлено 21/11/2018)

Чтобы преобразовать текст в число добавьте себе в код куда-нибудь такую функцию, например прям в класс Form

namespace WindowsFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public static int[,] TextToMatrix(string text)
        {
            var lines = text.Split(new string[] { Environment.NewLine, "\n" }, StringSplitOptions.RemoveEmptyEntries);
            var colsCount = lines[0].Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).Length;
            var rowsCount = lines.Length;

            var matrix = new int[rowsCount, colsCount];
            for (var i = 0; i < lines.Length; ++i)
            {
                var elements = lines[i].Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
                for (var j = 0; j < elements.Length; ++j)
                {
                    matrix[i, j] = int.Parse(elements[j]);
                }
            }
            return matrix;
        }

       // ...
    }
}

Допустим вы создали файлик в проекте matrix.txt, с содержимым:

1 2 3 4
6 7 8 9
10 12 12 13
14 15 16 17

указали у него свойство “Копировать в выходной каталог” на Всегда копировать

Теперь если вам нужно текст из файла преобразовать в матрицу вы пишите

namespace WindowsFormsApp
{
    public partial class Form1 : Form
    {
        // ...

        public static int[,] TextToMatrix(string text)
        {
            // ...
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var text = File.ReadAllText("matrix.txt"); // считали содержимое из файла, сохранили в переменную text 
            int[,] matrix = TextToMatrix(text); // превратили текст в матрицу
            
            // а дальше работаете с ней как обычно 
            // например тут я собираю матрицу в строку чтобы отобразить ее в сообщении
            string message = "";
            for (var i = 0; i < matrix.GetLength(0); ++i)
            {
                for (var j = 0; j < matrix.GetLength(1); ++j)
                {
                    message += matrix[i, j].ToString() + " ";
                }
                message += "\n";
            }

            MessageBox.Show(message);
        }
    }
} 

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

namespace WindowsFormsApp
{
    public partial class Form1 : Form
    {
        // ...

        public static int[,] TextToMatrix(string text)
        {
            // ...
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var text = txtMatrix.Text; // считали значение компоненты RichTextBox со свосйством (Name):txtMatrix, сохранили в переменную text 
            int[,] matrix = TextToMatrix(text); // превратили текст в матрицу
            
            // а дальше работаете все то же самое
            string message = "";
            for (var i = 0; i < matrix.GetLength(0); ++i)
            {
                for (var j = 0; j < matrix.GetLength(1); ++j)
                {
                    message += matrix[i, j].ToString() + " ";
                }
                message += "\n";
            }

            MessageBox.Show(message);
        }
    }
}