Управление внешними программами в среде .NET Framework
Андрей Колесов
Одна из обычных задач, возникающих при разработке ПО, — запуск и отслеживание состояния внешних программ. Традиционно при программировании в Windows для этого приходилось использовать средства Win API. Функции, появившиеся в технологии .NET, существенно упрощают решение данной задачи. Рассмотрим эти новые возможности на примере VB.NET.
Знакомство с классом Process
В классическом Visual Basic запуск внешних приложений выполнялся с помощью функции Shell, например, так:
ReturnID = Shell ("calc.exe", vbNormalFocus)
Эта функция в .NET была несколько улучшена по сравнению с VB 6.0, и ею по-прежнему можно пользоваться, однако ее возможности весьма ограниченны, прежде всего из-за того, что вызываемое приложение запускается в асинхронном режиме.
Вместо этого для работы с внешними программами в .NET лучше использовать класс Process, находящийся в пространстве имен System.Diagnostics. В простейшем случае запуск внешней программы будет выполняться с помощью метода Start; в данном примере для обработки указанного файла будет запускаться текстовый редактор (обычно это NotePad, установленный по умолчанию):
System.Diagnostics.Process.Start _ ("c:MYPATHMYFILE.TXT")
Метод Start, в свою очередь, возвращает объект Process. С его помощью можно получить ссылку на запущенный процесс, например, чтобы узнать его имя:
Dim myProcess As Process = _ Process.Start("c:MYPATHMYFILE.TXT") MsgBox(myProcess.ProcessName) |
Для управления параметрами запускаемого процесса можно воспользоваться объектом ProcessStartInfo из того же пространства имен:
Dim psInfo As New _ System.Diagnostics.ProcessStartInfo _ ("c:mypathmyfile.txt") ' устанавливаем стиль открываемого окна psInfo.WindowStyle = _ System.Diagnostics.ProcessWindowStyle.Normal Dim myProcess As Process = _ System.Diagnostics.Process.Start(psInfo) |
Объект ProcessStartInfo можно также получить с помощью свойства Process.Start:
Dim myProcess As _ System.Diagnostics.Process = _ New System.Diagnostics.Process() myProcess.StartInfo.FileName = _ "c:mypathmyfile.txt" myProcess.StartInfo.WindowStyle = _ System.Diagnostics.ProcessWindowStyle.Normal myProcess.Start() |
Предварительную установку параметров запускаемого процесса (имя файла, стиль окна и т. п.) можно выполнять и в режиме разработки (Design Time) через компонент Process, который следует добавить к форме из раздела Components панели Toolbar. В этом случае все параметры свойства StartInfo можно будет вводить в окне Properties (рис. 1).
Рис. 1. Управлять параметрами объекта Process можно в среде разработки.
|
Запуск процесса и ожидание его завершения
Самый простой способ ожидания завершения запущенного процесса — использовать метод Process.WaitForExit (однако нужно иметь в виду, что, когда вы применяете его из Windows Form, форма перестает реагировать на некоторые системные события, например, Close):
' создание нового процесса Dim myProcess As Process = _ System.Diagnostics.Process.Start _ ("c:mypathmyfile.txt") ' Ожидание его завершения myProcess.WaitForExit() ' вывод результатов MessageBox.Show _ ("Notepad был закрыт в: " & _ myProcess.ExitTime & "." & _ System.Environment.NewLine & _ "Код завершения: " & _ myProcess.ExitCode) ' закрытие процесса myProcess.Close() |
Здесь нужно обратить внимание на два момента. Во-первых, хотя запущенный процесс уже завершен, можно получить информацию о нем, например, узнать время его окончания или код завершения. Во-вторых, после завершения запущенного процесса все же нужно выполнить операцию его закрытия Process.Close, чтобы освободить память, отведенную под объект Process.
Метод WaitForExit можно использовать для ожидания завершения запущенного процесса в течение заданного интервала времени, а метод Kill — для его аварийного завершения:
' Ожидание в течение 5 с myProcess.WaitForExit(5000) ' Если процесс не завершился, то мы ' аварийно завершаем его If Not myProcess.HasExited Then myProcess.Kill() ' все же ждем его завершения myProcess.WaitForExit() End If |
Обратите внимание: после выполнения метода Kill мы опять ожидаем завершения процесса. Это нужно сделать, если мы хотим затем получить информацию о времени завершения и код завершения, — иначе при обращении к свойствам ExitTime или ExitCode будет выдана программная ошибка, так как запущенный процесс еще не успеет закончиться.
Запуск скрытого процесса
Довольно часто разработчику вообще не нужно, чтобы внешний процесс отражался в окне на экране монитора. Типичный случай — выполнение какой-то операции в сеансе MS-DOS, например, получение списка файлов заданного каталога. Приведенный ниже код выполняет подобную операцию без создания окна для запущенного процесса. Обратите внимание на содержание командной строки: Windows XP распознает "&&" как разделитель команд, позволяя записывать в одну строку сразу несколько команд.
Dim myProcess As Process = New Process() ' имя выходного файла Dim outfile As String = _ Application.StartupPath & _ "dirOutput.txt" ' адрес системного каталога Dim sysFolder As String = _ System.Environment.GetFolderPath _ (Environment.SpecialFolder.System) ' Имя запускаемой программы ' и строка аргументов myProcess.StartInfo.FileName = "cmd.exe" myProcess.StartInfo.Arguments = _ "C cd " & sysFolder & _ " && dir *.com >> " & Chr(34) & _ outfile & Chr(34) & " && exit" ' запуск процесса в скрытом окне myProcess.StartInfo.WindowStyle = _ ProcessWindowStyle.Hidden myProcess.StartInfo.CreateNoWindow = True myProcess.Start() myProcess.WaitForExit() 'ожидание |
Определение момента завершения процесса
Как мы видели в предыдущем примере, метод WaitForExit блокирует все события приложения, которое инициировало внешний процесс. Чтобы дать возможность работать главной программе, используется конструкция, которая в цикле проверяет состояние внешнего процесса и тут же вызывает метод Application.DoEvents, обеспечивающий обработку других событий данного приложения:
Do While Not myProcess.HasExited Application.DoEvents Loop |
Кстати, эффект, получаемый в данном случае при опросе свойства HasExit, в VB 6.0 можно было получить, обратившись к функции Win32 API GetModuleUsage.
Однако с точки зрения минимизации загрузки процессора более эффективный по сравнению с предыдущим примером вариант — инициализация события Exited класса Process. При этом нужно установить свойство Process.EnableRaisingEvents равным True (по умолчанию оно равно False) и создать манипулятор события, включая процедуру обработки события:
Private Sub Button4_Click _ (ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button4.Click Dim myProcess As Process = New Process() myProcess.StartInfo.FileName = _ "c:mypathmyfile.txt" ' разрешаем процессу ' инициализировать событие myProcess.EnableRaisingEvents = True ' Добавить манипулятор события Exited AddHandler myProcess.Exited, _ AddressOf Me.ProcessExited ' запуск процесса myProcess.Start() End Sub Friend Sub ProcessExited _ (ByVal sender As Object, _ ByVal e As System.EventArgs) ' процедура обработки события ' (завершение процесса) Dim myProcess As Process = _ DirectCast(sender, Process) MessageBox.Show _ ("Процесс завершился в" & _ myProcess.ExitTime & _ System.Environment.NewLine & _ "Код завершения: " & _ myProcess.ExitCode) myProcess.Close() End Sub |
Здесь нужно обратить внимание на одну потенциальную опасность: если вызванный процесс зависнет, то с приложением тоже могут возникнуть проблемы. Чтобы избежать этого, можно включить контроль времени ожидания по таймеру.
Обмен информацией с внешним процессом
Иногда возможностей передачи информации в вызываемый процесс через простую командную строку оказывается явно недостаточно. Бывает также, что получение от него результирующих данных через файл, как это сделано в предыдущем примере, не представляется оптимальным вариантом. Порой для обмена данными требуются механизмы прямого взаимодействия основного и вызываемого приложений.
Для вызываемых программ, которые поддерживают механизмы StdIn, StdOut и StdErr (например, консольных приложений), можно использовать объекты StreamWriter и StreamReader для записи и чтения данных. Чтобы это сделать, нужно установить свойства RedirectStandardInput, RedirectStandardOutput и RedirectStandardError объекта ProcessStartInfo равными True. Затем, после запуска внешнего процесса, нужно использовать свойства StandardInput, StandardOutput и StandardError объекта Process для привязки потока ввода-вывода к объектам StreamReader и StreamWriter.
Еще одно замечание: по умолчанию среда .NET Framework использует функцию Win32 ShellExecute для взаимодействия с внешним процессом, но когда вы переопределяете потоки ввода-вывода, перед запуском приложения нужно установить свойство ProcessStartInfo.UseShellExecute равным False.
В приведенном ниже примере создается невидимое окно, формируется список файлов заданного каталога, а результаты выводятся в окне MessageBox (рис. 2)
Рис. 2. Информация, полученная из вызванного процесса через объект StdOut.
|
' обмен данными с внешним процессом через ' функции StdIn, StdOut и StdErr Dim s As String Dim myProcess As Process = New Process() ' описание и запуск процесса myProcess.StartInfo.FileName = "cmd.exe" myProcess.StartInfo.UseShellExecute = False myProcess.StartInfo.CreateNoWindow = True myProcess.StartInfo.RedirectStandardInput = True myProcess.StartInfo.RedirectStandardOutput = True myProcess.StartInfo.RedirectStandardError = True myProcess.Start() ' описание объектов передачи данных Dim sIn As System.IO.StreamWriter = _ myProcess.StandardInput sIn.AutoFlush = True Dim sOut As System.IO.StreamReader = _ myProcess.StandardOutput Dim sErr As System.IO.StreamReader = _ myProcess.StandardError ' передача входных данных sIn.Write("dir c:drv*.*" & _ System.Environment.NewLine) sIn.Write("exit" & _ System.Environment.NewLine) ' получаем результат выполнения команды DIR s = sOut.ReadToEnd() If Not myProcess.HasExited Then myProcess.Kill() End If MessageBox.Show("Окно команды 'dir' " & _ "было закрыто в: " & _ myProcess.ExitTime & "." & _ System.Environment.NewLine & _ "Код завершения: " & _ myProcess.ExitCode) sIn.Close() sOut.Close() sErr.Close() myProcess.Close() ' смотрим результат работы процесса DIR MessageBox.Show(s) |
Если вызываемое приложение не использует StdIn, можно применить метод SendKeys для передачи данных, вводимой с клавиатуры. Например, следующий код вызывает NotePad и вводит в него некоторый текст:
Dim myProcess As Process = New Process() myProcess.StartInfo.FileName = "notepad" myProcess.StartInfo.WindowStyle = _ ProcessWindowStyle.Normal myProcess.Start() ' Ждем 1 с, чтобы NotePad был готов ' к вводу данных myProcess.WaitForInputIdle(1000) if myProcess.Responding Then System.Windows.Forms.SendKeys.SendWait( _ "Этот текст был введен " & _ "с помощью метода " & _ "System.Windows.Forms.SendKeys.") Else myProcess.Kill() End If |
Метод SendKeys позволяет передавать коды любых клавиш, включая Alt, Ctrl и Shift. Таким образом можно передавать комбинации клавиш для выполнения стандартных команд, например, загрузки или сохранения файлов, управления командами меню и т. п. Но нужно помнить, что этот метод посылает код только в активное окно приложения, и если нужное окно потеряет фокус, могут возникнуть проблемы. Именно поэтому мы использовали метод Process.WaitForInputIdle, чтобы проверить, готово ли приложение к получению информации. Для NotePad времени ожидания в 1 с вполне достаточно, но для других приложений его, возможно, придется увеличить.
* * *
Итак, хотя функция Shell по-прежнему работает в .NET Framework, класс System.Diagnostics.Process предоставляет гораздо больше возможностей для взаимодействия с внешними процессами. Переадресуя потоки StdIn, StdOut и StdErr, можно наладить двусторонний обмен данными с приложением, а применяя метод SendKeys, можно вводить информацию (в том числе управляющие команды меню) в программы, которые не используют StdIn.