Управление внешними программами в среде .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.

