前言
控制台程序(DOS)程序是一种广泛使用的用于处理后台数据的应用程序。由于很多程序主要用于数据处理而不需要面向终端用户,所以不需要设计用户界面。C#是一门非常强大的编程语言,是当前最好的桌面应用程序界面制作语言之一。所以我们在进行用户程序设计的时候,可以使用C#进行软件界面的设计,然后调用一些功能模块,尤其是已经存在可利用模块。
目前,关于C#调用DOS程序的文章有很多,但是却存在很多问题。
- 内容简单肤浅:基本就是只讲如何在C#中调用,仅限于语法层面。
- 缺少解释说明:经常是只直接贴代码,而没有解释说明。
- 内容片面有限:基本只讲如何调用,而相关阻塞、线程等内容都没有说明。
- 缺少技术细节,如怎样显示调用程序的窗口,阻塞是如何发生的,如何进行处理等。
本文针对C#调用DOS程序的多方面技术细节,通过实验的形式进行详细的分析、进而给出结论,帮助读者快速全面掌握这项调用技术。在本文中,为了描述时便于区分,我们将被调用的DOS程序称之为控制台程序,简称 CP(Console Program),而调用控制台程序的程序称之为主程序,简称 MP (Main Program)。
准备工作
首先,我们编写了一个控制台程序 ConsoleProgram.exe,其Main函数代码如下所示。此程序首先输出参数的长度,然后模拟用时2秒进行数据处理,最后返回 Done 表示任务完成。
static void Main(string[] args)
{
// 处理时间为 2 秒,通过延时实现
Console.WriteLine("Processing the inputs of length " + args.Length);
System.Threading.Thread.Sleep(2000);
// 输出结果完成
Console.WriteLine("Done");
}
简单调用
如果只考虑能够让CP运行起来的简单调用,那么只需要以下代码即可。
public void SimpleInvoke()
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.FileName = @"C:\data\ConsoleProgram.exe";
p.StartInfo.Arguments = @"C:\Program Files\Test D:\Output";
p.Start();
}
其中 FileName 为CP可执行文件名称,包括完全路径和文件名称。由于在DOS环境下,对空格和中文都有要求(具体请参考相关文章,不在本文设计范围内),所以我们用以下三种路径来进行测试,其中,Dir1代表简单路径,Dir2代表有空格的路径,Dir3为代表有中文的路径。
Dir1: C:\data\ConsoleProgram.exe
Dir2: C:\data\20181121 Test\ConsoleProgram.exe
Dir3: C:\data\20181121 控制台程序测试\ConsoleProgram.exe
经测试,三种路径都可以正常调用程序,说明C#在调用DOS程序时,已经对路径问题进行了处理,我们可以忽略路径的名称问题。
但是,关于参数则出现了问题,在输出界面上,显示的内容如下:
Processing the inputs of length 3
这说明,参数的长度为3。通过调试查看args
可以发现这三个参数分别为 C:\\Program
,Files
和D:\\Outout
。显然,第一个参数 C:\Program Files
由于中间有空格,被DOS拆分成2个参数来处理了。为了避免这样的问题,根据DOS的要求,带有空格的路径名称需要用双引号包起来,所以可以将第一个参数修改为 "C:\Program Files"
,即:
p.StartInfo.Arguments = "\"C:\\Program Files\" D:\\Outout";
再次运行,即可得到正确的结果,即参数长度为2。这说明参数路径的空格并没有进行相应的处理,所以我们在传参的时候,需要手工处理参数中包括的空格。
可接收结果的调用
在调用了DOS程序后,很多DOS程序都是有返回结果的。但是在以上的简单调用时,由于CP的窗体是独立于MP且它们之间任何直接的关联,所以我们是接收不到结果的。为了解决这个问题,我们可以通过建立输出连接的方式来接收数据。
具体实现办法如下代码所示。
public void ReturnableInvoke()
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.FileName = @"C:\data\ConsoleProgram.exe";
p.StartInfo.UseShellExecute = false; // 关键行1
p.StartInfo.RedirectStandardOutput = true; // 关键行2
p.Start();
string output = p.StandardOutput.ReadToEnd();
Console.WriteLine(output);
}
执行过以后,即可在控制台获得CP的输出数据。在以上代码中,我们修改了p.StartInfo的几个属性,分别介绍如下。
- UseShellExecute
由于本来输出到控制台的数据,现在输出到调用的程序中来,所以必需将此值改为 false。官方文档中,也明确提出:要重定向 IO 流,Process 对象必须将 UseShellExecute 属性设置为 False。 - RedirectStandardOutput
通过名称可以看出,这个变量的作用是决定是否重新定位输出流,只有修改了这个值以后,调用程序才可以接收到数据。 - StandardOutput
控制台程序的输出,可以调用ReadToEnd()
方法获取所有的输出内容。
知识扩展:隐藏控制台程序界面
通常情况下,我们在C#程序中调用控制台程序是不希望显示出控制台的黑色界面的。隐藏此窗实现起来非常简单,只需要将 p.StartInfo.CreateNoWindow
设置为true
即可。但是要注意,隐藏窗体也必需在UseShellExecute
属性为false
时才可以生效。
阻塞
使用以上的代码接收处理的数据时,主程序将会在调用时发生阻塞,即主程序需要等待控制台程序退出后才会继续响应用户操作。这是因为主程序调用p.StandardOutput.ReadToEnd()
接收数据的前提条件是必需让控制台完成数据的处理并输出后,才能收到数据。所以程序会卡在这里,直到数据处理完成。这时候,如果控制台程序出现问题,卡的时间很长,会影响主程序的用户体验。我们这时候有两种办法来处理这个问题:超时中止和异步调用。
超时中止
p.Start();
p.WaitForExit(1000);
if(!p.HasExited)
p.Kill();
p.StandardOutput.ReadToEnd();
WaitForExit:等待一段时间,系统将卡在这里一段时间。时间长度由括号内的参数决定,单位为毫秒。
在以上代码段中,首先调用控制台程序,这里调用程序会在启动后继续执行,然后运行WaitForExit(1000)阻塞1000毫秒,这里有两种可能:
- 阻塞结束前执行完成
这时候,调用程序会中止阻塞,提前让调用程序继续执行下面的代码。 - 阻塞结束后执行完成
这时候,如果不想再等待,可以调用 p.Kill结束调用程序。
关于如何判断是否已经结束,可以使用 HasExited 属性进行判断,如果其值为真则表示程序执行完成顺利退出;否则表示仍然在执行中,可以使用Kill()中止CP的运行。以上的工作方式可以理解为在很多程序中都经常用到的 timeout.
异步调用
在以上的代码中,由于阻塞,我们需要等待程序结果后,才能继续执行调用程序。在实际的使用中,如果调用程序是带有UI界面的程序,那么界面会卡住,无法接受用户响应。所以为了解决此问题,我们需要进行异步调用,即在调用后,返回调用程序,然后等控制台程序计算结果完成后,再返回结果,具体做法如下所示。
p.OutputDataReceived += P_OutputDataReceived;
p.Start();
p.BeginOutputReadLine();
OutputDataReceived 是一个消息接收时触发的事件,事件回调函数包括DataReceivedEventArgs 参数,其中有个 Data 属性即是控制台程序返回的输出字符串。需要注意的是,在启动控制台程序后,需要调用 BeginOutputReadLine() 函数,以通知调用函数发回数据。由于它是一个异步函数,在调用后调用程序仍然会继续向前执行,不会导致发生阻塞导致界面卡死。当DP有任何输出时 ,会发送给MP。注意:由于数据是以行的形式读取的,所以每发送一行数据,OutputDataReceived 事件会被触发一次。
后记
本方从2018/11/21开始写,中间断断续续写到现在……