平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C#调用第三方应用程序怎么实现方法自动化和参数传递”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
调用第三方应用程序
从实现思路看,执行一个外部的第三方应用的原理,和调用cmd执行命令是一样的。只不过能够添加程序的启动参数(StartInfo.Arguments)等。
在这个场景下,调用其他应用程序,和调用cmd执行命令行也有所区别。通常调用cmd是为了执行完某条或某几条命令,执行完就结束了。而调用第三方应用,在其启动后,关于程序的操作、处理、是否有输入和输出,不同的程序有着不同的情况,不同需求和不同人的操作也会不同。能够尝试采用之前cmd的调用代码执行第三方程序,就能感受到很大不同,尤其是StandardOutput读取输出,会一直等待。
落到代码里,因此,调用第三方程序,更多的是启动第三方程序,随后让用户去操作;或者针对某特定的程序,启动后借助代码准确执行特定的步骤,即自动化操作,然后关闭应用(这时涉及到自动化的部分[ui自动化、输入输出自动化]),甚至需多线程,当前线程采用Process打开应用,另一个线程操作应用等。
结合项目来看,所以,目前难以有比较通用的方式,但是能够写一个启动方法,启动第三方软件,设置等不等待软件运行结束、获不拿到输出(如果有的话)。后面更多的应该了解Process的采用。
在这个场景下,如下所示,采用下面的方式启动一个可执行程序。根据需是否有启动参数、是否等待结束、是否拿到输出等:
/// <summary>
/// 执行外部exe程序
/// </summary>
public static class ExecApp
{
/// <summary>
/// 启动一个可执行程序,执行参数为arguments。启动一个关联的子进程,并且等待子进程的处理返回【一种有限的方式运行其他程序】
/// 注意,如果是等待结束的情况下,必须保证会自动结束,或者最后会关闭/退出程序
/// </summary>
/// <param name="appPath"></param>
/// <param name="arguments">启动参数</param>
/// <param name="waitComplete">是否等待结束,默认false,启动程序后就结束不再管理;若为true,则必须保证会执行结束,或者手动关闭/退出程序,否则一直等待</param>
/// <param name="getOuput">是否获取输出结果,waitComplete为true时,true获取输出结果才有效,并且要确保程序有输出,否则会一直等待</param>
/// <returns></returns>
public static async Task<ExecResult> StartExecutAsync(string appPath,string arguments=null, bool waitComplete= false, bool getOuput=false)
{
using (Process p = new Process())
{
var result = new ExecResult();
try
{
p.StartInfo.FileName = appPath;
p.StartInfo.Arguments = arguments;
p.StartInfo.UseShellExecute = false; //设置false只能启动一个程序
p.StartInfo.RedirectStandardInput = false;
p.StartInfo.RedirectStandardOutput = getOuput; //由调用程序获取输出信息
p.StartInfo.RedirectStandardError = getOuput; //重定向标准错误输出
p.StartInfo.CreateNoWindow = false; //显示程序窗口
//p.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
p.Start();//启动程序执行
if (waitComplete)
{
if (getOuput)
{
// 获取输出【确保程序有输出,否则会一直等待】
result.Output = await p.StandardOutput.ReadToEndAsync();
result.Error = await p.StandardError.ReadToEndAsync();
}
p.WaitForExit();//等待程序执行完退出进程。应在最后调用
}
p.Close();
}
catch (Exception ex)
{
result.ExceptError = ex;
}
return result;
}
}
}
新建项目ExecAppTest,用来测试执行外部程序。
如下所示测试执行。
//var result= await ExecApp.StartExecutAsync(@"C:WINDOWSsystem32cmd.exe");
var result= await ExecApp.StartExecutAsync(@"C:WINDOWSsystem32cmd.exe",waitComplete:true,getOuput:true);
MessageBox.Show($"{result.Output}");
Process进程类介绍和采用
Process打开一个文档或文件、启动一个程序
结合项目来看,Process能够采用默认应用打开一个与其文件类型(后缀)关联的任何文档。比如打开以为文本文档、打开一个图片文件、打开一个url地址等。
Process.Start("www.bing.com");
Process.Start("my.png");
Process.Start(@"C:WINDOWSsystem32cmd.exe");
Process.Start("explorer.exe", @"c:/"); //用资源管理器打开C:/
拿到进程中的多个模块:
var mList = Process.GetCurrentProcess().Modules;
foreach (ProcessModule m in mList)
{
Console.WriteLine(string.Format("ModuleName is {0} t ModuleURL is {1} t ModuleVersion is {2}", m.ModuleName, m.FileName, m.FileVersionInfo.FileVersion));
}
ProcessStartInfo进程启动信息
ProcessStartInfo用来设置进程其实的一些初始信息,比如启动参数(Arguments)、工作目录(WorkingDirectory)、是否采用系统Shell(UseShellExecute)、是否显示窗口(CreateNoWindow)窗口的样式状态(WindowStyle)、重定向输入输出、输出的编码(StandardOutputEncoding)等
落到代码里,ProcessStartInfo Class能够查看更多的启动信息设置。
Process.StartInfo属性就是ProcessStartInfo,能够直接新建实例采用。
也能够新建ProcessStartInfo对象,所谓进程Process.Start()启动时的参数。
// 启动一个隐藏窗口的程序
ProcessStartInfo psi = new ProcessStartInfo()
{
FileName= fileName,
Arguments = arguments,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(psi);
ProcessStartInfo.UseShellExecute、ProcessStartInfo.WorkingDirectory和ProcessStartInfo.ErrorDialog
ProcessStartInfo.UseShellExecute=false允许重定向输入、输出和错误流。但是,此时只能采用Process对象打开可执行程序。
UseShellExecute的关键字shell表示图形shell(壳),而不是命令shell(如bash或sh),采用它能够让用户启动图形应用或打开文档。
UserName非空时,UseShellExecute必须为false,否则报错InvalidOperationException。
- 在这个场景下,当采用操作系统shell启动进程时(
UseShellExecute=true),能够启动任何文档(包括可执行程序。只要注册关联了文件类型就行),同时在文件上执行操作(比如采用Process对象打印)。
若ErrorDialog=true,则UseShellExecute必须为true;如果WindowStyle=ProcessWindowStyle.Hidden,UseShellExecute也必须为true。
在这个场景下,UseShellExecute 在.NET Framework中的默认值为true;.NET Core中为false。
ProcessStartInfo.ErrorDialog表示进程无法启动时是否显示对话框提醒。
UseShellExecute的取值不同,WorkingDirectory也会有所不同。
如果UseShellExecute为true, WorkingDirectory表示可执行程序的位置;
当UseShellExecute为false时,WorkingDirectory由启动的process对象采用。当UseShellExecute为false时,FileName能够是可执行文件的绝对路径,也能够是系统PATH环境变量指定的文件夹中找到的轻松的可执行程序名称。
也就是对于cmd的调用,useshellexecute=false时,FileName能够直接指定为cmd.exe或cmd即可。
WorkingDirectory结合项目来看,工作目录是一个很拥有的属性,UseShellExecute为false时一定要指定正确(通常为可执行程序的所在路径,即要启动的进程的工作目录)。
进程退出、标准输出和错误输出事件
Process.OutputDataReceived事件用来接受标准输出。上面的多个命令行执行的代码已有采用。Process.ErrorDataReceived事件用来接受错误输出。上面的多个命令行执行的代码已有采用。Process.Exited事件用来检测变化外部进程是否退出,能够避免WaitForExit()的线程阻塞。在结束事件中处理结束后的操作。Exited事件的启用,需设置EnableRaisingEvents=true。
理解这一步时,比如下面,打开记事本程序,随后在Exited事件方法中处理其关闭后的操作,不阻塞当前线程的其他操作、
Process process = new Process();
process.StartInfo.FileName = @"C:WindowsNotepad.exe";// process.StartInfo.FileName = "Notepad";
process.EnableRaisingEvents = true;
process.Exited += Process_Exited;
process.Start();
//...............
private void Process_Exited(object sender, EventArgs e)
{
//Exited事件处理代码,外部程序退出后激活,可以执行相关操作
MessageBox.Show("Notepad.exe运行完毕");
}
总结
从实现思路看,总的来说,C#调用程序适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。