C#调用控制台(DOS)程序详解(完成度:70%)

前言

控制台程序(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:\\ProgramFilesD:\\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开始写,中间断断续续写到现在……

猜你喜欢

转载自blog.csdn.net/weixin_43145361/article/details/84313108