На занятиях по системному программированию попался проблемный код. Пока искал решение, заметил, что этот вопрос поднимают в интернетах нередко. Вероятно, код, похожий на описанный здесь, часто используют в учебниках и статьях для начинающих. Пишем на языке программирования C# в операционной системе «Windows 10». Используем среду разработки «Visual Studio Community 2022».

Мы сейчас начали изучать работу с процессами. Задача простая: создать приложение с окном, в котором будут две кнопки: «Старт» и «Стоп». Кнопка «Старт» запускает процесс приложения «Калькулятор», которое входит в состав системы Windows. Кнопка «Стоп» закрывает приложение «Калькулятор».

Для создания приложения использовали в «Visual Studio» шаблон «Приложение Windows Forms (Майкрософт)», по-английски «Windows Forms App». Этот шаблон подразумевает работу на платформе «.NET». Конкретно мы сейчас используем «.NET 8.0». Вот как у меня выглядит окно приложения с кнопками:

Запускатель калькулятора

По указанному шаблону код приложения размещен в трех файлах: Program.cs, Form1.Designer.cs и Form1.cs. Мы дописываем свой код в файл Form1.cs, вот как он у нас в итоге выглядит:

using System.Diagnostics;

namespace Calc_runner
{
    public partial class Form1 : Form
    {
        Process myProcess;

        public Form1()
        {
            InitializeComponent();
        }

        private void start_Click(object sender, EventArgs e)
        {
            myProcess = Process.Start("calc.exe");
        }

        private void stop_Click(object sender, EventArgs e)
        {
            myProcess.CloseMainWindow();
            myProcess.Close();
        }
    }
}

Описание проблемы

У меня этот код компилируется без ошибок. После запуска этого приложения без проблем получается запустить приложение «Калькулятор», входящее в состав системы «Windows 10», с помощью кнопки «Старт». Однако, нажатие на кнопку «Стоп» не дает видимого результата. Повторное нажатие на кнопку «Стоп» приводит к краху приложения из-за неотловленного исключения.

Эту задачу с похожим кодом мы взяли из наших методических материалов. Судя по тексту методических материалов, они были написаны довольно давно, может, лет семь назад. Вероятно, раньше (для систем Windows до 10-й версии) этот код работал. В нашей системе «Windows 10» часть кода, закрывающая приложение «Калькулятор», не работает.

Пути к решению

Сначала мы решили просто взять другое приложение для открытия-закрытия и выбрали приложение «Блокнот», входящее в состав Windows 10. Соответствующий процесс называется notepad.exe. То есть в коде выше мы просто заменили calc.exe на notepad.exe.

После этого кнопка «Стоп» сработала, успешно закрыв приложение «Блокнот». То есть вышеприведенный код в принципе рабочий, а проблема в данном случае не в коде, а в особенностях приложения «Калькулятор».

Повторное нажатие на кнопку «Стоп» по-прежнему приводит к краху нашего приложения из-за неотловленного исключения. Тут тоже всё разъяснилось достаточно быстро: исключение вызывается в методе CloseMainWindow при попытке закрыть уже закрытый процесс. Обойти это несложно, вставив в код соответствующую проверку, например:

        private void stop_Click(object sender, EventArgs e)
        {
            if (myProcess != null)
            {
                myProcess.CloseMainWindow();
                myProcess.Close();
                myProcess = null;
            }
        }

Особенности приложения «Калькулятор»

Как оказалось, приложение «Калькулятор», входящее в состав системы Windows 10, сильно изменили по сравнению с этим же приложением, входящим в состав систем Windows до 10-й версии. В наших экземплярах систем Windows 10 тоже есть процесс calc.exe, как и раньше, но это уже не само приложение «Калькулятор», а лишь заглушка, оставленная для обратной совместимости с предыдущими версиями систем Windows.

В наших «Windows 10» процесс calc.exe запускается, затем запускает другой процесс — CalculatorApp.exe, и после этого calc.exe сразу же автоматически закрывается. Таким образом, в методе stop_Click в объекте myProcess у нас содержится информация о закрывшемся процессе calc.exe, а не о нужном нам процессе CalculatorApp.exe. Поэтому указанный выше код никак не сможет в данной ситуации закрыть приложение «Калькулятор».

Первая попытка решения

Один из способов решения описанной проблемы — найти запущенный процесс CalculatorApp.exe и закрыть его. Для этого можно использовать метод GetProcessesByName, который может найти нужный процесс по указанному имени. Поскольку система позволяет запускать несколько процессов с одним и тем же именем, этот метод возвращает не один объект, представляющий процесс, а массив таких объектов. Мы переписали код метода stop_Click следующим образом:

        private void stop_Click(object sender, EventArgs e)
        {
            Process[] procArr = Process.GetProcessesByName("CalculatorApp");
            if (procArr.Length > 0)
            {
                procArr[0].CloseMainWindow();
                procArr[0].Close();
            }
        }

Однако, этот код у меня по-прежнему не работает.

Еще одна особенность приложения «Калькулятор»

После проверок выяснилось, что метод GetProcessesByName отрабатывает корректно и успешно находит нужный процесс или процессы, если их с указанным именем запущено несколько. В данном случае проблема — в методе CloseMainWindow, который не срабатывает, если в свойстве MainWindowHandle (дескриптор главного окна процесса) объекта класса Process содержится значение 0.

Мы до конца так и не разобрались, почему для приложения «Калькулятор» в данном случае в свойстве MainWindowHandle содержится значение 0. Обычно такое значение говорит о том, что у процесса нет главного окна (то есть приложение не имеет графического интерфейса или по еще каким-либо другим причинам). В данном случае у приложения «Калькулятор» есть окно, оно видно на экране, но свойство MainWindowHandle всё равно содержит значение 0.

Работающее решение

В документации к методу CloseMainWindow сказано, что этот метод лишь отправляет приказ (сообщение) о закрытии целевому процессу. Однако, процесс может по разным причинам отказаться выполнять этот приказ (сообщение). В таких случаях процесс можно закрыть насильно с помощью другого метода — Kill (при этом могут потеряться данные, с которыми в этот момент работало приложение, в отличие от работы метода CloseMainWindow). Нас это в данном случае устроило: ничего страшного, если с закрытием калькулятора будут потеряны какие-то текущие вычисления.

Мы переписали программу, вот итоговый результат:

using System.Diagnostics;

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

        private void start_Click(object sender, EventArgs e)
        {
            Process myProcess = Process.Start("calc.exe");
        }

        private void stop_Click(object sender, EventArgs e)
        {
            Process[] procArr = Process.GetProcessesByName("CalculatorApp");
            if (procArr.Length > 0)
            {
                procArr[0].Kill();
            }
            else
            {
                MessageBox.Show("Не могу найти открытый калькулятор, " +
                                "закрывать нечего!");
            }
        }
    }
}

Эта программа работает. Она работает, даже если приложений «Калькулятор» открыто несколько. В последнем случае все открытые приложения «Калькулятор» можно закрыть одно за другим, последовательно нажимая на кнопку «Стоп». Если нажать кнопку «Стоп» до открытия приложения «Калькулятор» или после закрытия всех открытых ранее приложений «Калькулятор», программа выдаст сообщение «Не могу найти открытый калькулятор, закрывать нечего!» в отдельном окошке.