如何从命令中获取输出以实时显示在窗体上的控件中? [英] How do I get output from a command to appear in a control on a Form in real-time?

查看:24
本文介绍了如何从命令中获取输出以实时显示在窗体上的控件中?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

从网络上的各种来源,我整理了以下代码,用于通过 CMD.exe 执行命令并捕获 STDOUTSTDERR.

公共静态类Exec{公共委托 void OutputHandler(String line);//<总结>///在子进程中运行命令///</总结>///<param name="path">执行命令的目录</param>///<param name="cmd">要执行的命令</param>///<param name="args">命令的参数</param>///<param name="hndlr">命令输出处理程序(如果没有则为null)</param>///<param name="noshow">如果没有窗口显示为真</param>///<returns>从执行的命令中退出代码</returns>public static int Run(String path, String cmd, String args,OutputHandler hndlr = null,布尔值 noshow = true){//假设错误int ret = 1;//创建一个进程使用 (var p = new Process()){//使用 CMD.EXE 运行命令//(这样我们可以通过管道将 STDERR 传输到 STDOUT,以便它们可以一起处理)p.StartInfo.FileName = "cmd.exe";//设置工作目录(如果提供)if (!String.IsNullOrWhiteSpace(path)) p.StartInfo.WorkingDirectory = path;//指示命令和参数p.StartInfo.Arguments = "/c "" + cmd + " " + args + "" 2>&1";//处理 noshow 参数p.StartInfo.CreateNoWindow = noshow;p.StartInfo.UseShellExecute = false;//查看是否提供了处理程序如果(hndlr != 空){//重定向 STDOUT 和 STDERRp.StartInfo.RedirectStandardOutput = true;p.StartInfo.RedirectStandardError = true;//使用自定义事件处理程序来捕获输出使用 (var outputWaitHandle = new AutoResetEvent(false)){p.OutputDataReceived += (sender, e) =>{//查看是否有数据if (e.Data == null){//信号输出处理完成outputWaitHandle.Set();}别的{//将字符串传递给字符串处理程序hndlr(e.Data);}};//启动进程p.Start();//开始异步读取p.BeginOutputReadLine();//等待进程终止p.WaitForExit();//等待输出处理完成信号outputWaitHandle.WaitOne();}}别的{//启动进程p.Start();//等待进程终止p.WaitForExit();}//获取退出代码ret = p.ExitCode;}//返回结果返回 ret;}//<总结>///在子进程中运行命令并在变量中返回输出///</总结>///<param name="path">执行命令的目录</param>///<param name="cmd">要执行的命令</param>///<param name="args">命令的参数</param>///<param name="outp">包含输出的变量</param>///<returns>从执行的命令中退出代码</returns>public static GetOutputReturn GetOutput(String path, String cmd, String args){GetOutputReturn ret = new GetOutputReturn();ret.ReturnCode = Run(path, cmd, args, (line) =>{ret.Output.AppendLine(line);});返回 ret;}}公共类 GetOutputReturn{public StringBuilder Output = new StringBuilder();public int ReturnCode = 1;}

我可以通过以下三种不同的方式在控制台应用程序中使用它:

static void Main(string[] args){int ret;Console.WriteLine("在没有捕获和窗口的情况下执行目录");ret = Exec.Run(@"C:", "dir", "");Console.WriteLine("执行返回" + ret);Console.WriteLine("按回车继续...");Console.ReadLine();Console.WriteLine("在没有捕获和窗口的情况下执行目录");ret = Exec.Run(@"C:", "dir", "", null, false);Console.WriteLine("执行返回" + ret);Console.WriteLine("按回车继续...");Console.ReadLine();Console.WriteLine("执行带有捕获且没有窗口的目录");var 结果 = Exec.GetOutput(@"C:", "dir", "");Console.WriteLine(results.Output.ToString());Console.WriteLine("执行返回" + results.ReturnCode);Console.ReadLine();Console.WriteLine("实时捕获执行目录且无窗口");ret = Exec.Run(@"C:", "dir", "", ShowString);Console.WriteLine("执行返回" + ret);}公共委托 void StringData(String str);静态无效 ShowString(String str){Console.WriteLine(str);}公共委托 void StringData(String str);静态无效 ShowString(String str){Console.WriteLine(str);}

第一次运行不收集任何输出,只显示退出代码.
第二次运行不会收集任何输出,但会显示窗口.
输出实时出现在控制台窗口中的效果.
第三次运行使用 GetOutput 来收集输出.
这样做的效果是直到运行完成才会出现输出.
最后一次运行使用处理程序实时接收和显示输出.
从外观上看,这看起来像第二次运行,但非常不同.
对于接收到的每一行输出,调用 ShowString.
显示字符串只是显示字符串.
然而,它可以对数据做任何它需要的事情.

我正在尝试调整最后一次运行,以便我可以使用命令的输出实时更新文本框.我遇到的问题是如何在正确的上下文中使用它(因为没有更好的术语).因为OutputHandler 是异步调用的,所以它必须使用InvokeRequired/BeginInvoke/EndInvoke 机制来与UI 线程同步.我对如何使用参数执行此操作有一点问题.在我的代码中,文本框可能是选项卡控件中的多个文本框之一,因为可能会发生多个背景运行".

到目前为止我有这个:

private void btnExecute_Click(object sender, EventArgs e){//获取当前选中的标签页var page = tcExecControl.SelectedTab;//获取文本框(总是页面上的第三个控件)var txt = (TextBox)page.Controls[2];//创建字符串处理程序var prc = new Exec.OutputHandler((String line) =>{如果(txt.InvokeRequired)txt.Invoke(new MethodInvoker(() =>{ txt.Text += 行;}));否则 txt.Text += 行;});//命令和参数始终是页面上的第一个和第二个控件var result = Exec.Run(@"C:", page.Controls[0].Text, page.Controls[1], prc);}

但这似乎不起作用.我没有看到 txtBox 的任何输出.
实际上程序基本上挂在处理程序中.

如果我将代码更改为使用 GetOutput,然后将结果输出写入文本框,则一切正常.所以我知道我已经正确设置了命令.使用调试器,我可以在if (txt.InvokeRequired)"行上设置断点,我看到第一行输出正确.此时代码采用 if 语句的真实路径,但如果我在 txt.Text += 行; 行上设置断点,它永远不会到达那里.

谁能帮帮我?我确定我遗漏了一些东西.

解决方案

对本示例中代码执行内容的简要说明:

首先运行 shell 命令 (cmd.exe),使用 start/WAIT 作为参数.或多或少与 /k 的功能相同:控制台在没有任何特定任务的情况下启动,在发送命令时等待处理.

From various sources on the web, I have put together the following code for executing a command via CMD.exe and capturing output from STDOUT and STDERR.

public static class Exec
{
    public delegate void OutputHandler(String line);

    // <summary>
    /// Run a command in a subprocess
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="hndlr">Command output handler (null if none)</param>
    /// <param name="noshow">True if no windows is to be shown</param>
    /// <returns>Exit code from executed command</returns>
    public static int Run(String path, String cmd, String args,
                          OutputHandler hndlr = null, Boolean noshow = true)
    {
        // Assume an error
        int ret = 1;
        // Create a process
        using (var p = new Process())
        {
            // Run command using CMD.EXE
            // (this way we can pipe STDERR to STDOUT so they can get handled together)
            p.StartInfo.FileName = "cmd.exe";
            // Set working directory (if supplied)
            if (!String.IsNullOrWhiteSpace(path)) p.StartInfo.WorkingDirectory = path;
            // Indicate command and arguments
            p.StartInfo.Arguments = "/c "" + cmd + " " + args + "" 2>&1";
            // Handle noshow argument
            p.StartInfo.CreateNoWindow = noshow;
            p.StartInfo.UseShellExecute = false;
            // See if handler provided
            if (hndlr != null)
            {
                // Redirect STDOUT and STDERR
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.RedirectStandardError = true;
                // Use custom event handler to capture output
                using (var outputWaitHandle = new AutoResetEvent(false))
                {
                    p.OutputDataReceived += (sender, e) =>
                    {
                        // See if there is any data
                        if (e.Data == null)
                        {
                            // Signal output processing complete
                            outputWaitHandle.Set();
                        }
                        else
                        {
                            // Pass string to string handler
                            hndlr(e.Data);
                        }
                    };
                    // Start process
                    p.Start();
                    // Begin async read
                    p.BeginOutputReadLine();
                    // Wait for process to terminate
                    p.WaitForExit();
                    // Wait on output processing complete signal
                    outputWaitHandle.WaitOne();
                }
            }
            else
            {
                // Start process
                p.Start();
                // Wait for process to terminate
                p.WaitForExit();
            }
            // Get exit code
            ret = p.ExitCode;
        }
        // Return result
        return ret;
    }

    // <summary>
    /// Run a command in a subprocess and return output in a variable
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="outp">Variable to contain the output</param>
    /// <returns>Exit code from executed command</returns>
    public static GetOutputReturn GetOutput(String path, String cmd, String args)
    {
        GetOutputReturn ret = new GetOutputReturn();
        ret.ReturnCode = Run(path, cmd, args, (line) =>
                             {
                               ret.Output.AppendLine(line);
                             });
        return ret;
    }
}

public class GetOutputReturn
{
    public StringBuilder Output = new StringBuilder();
    public int ReturnCode = 1;
}

I am able to use this in a console app in three different manners as follows:

static void Main(string[] args)
{
    int ret;
    Console.WriteLine("Executing dir with no capture and no window");
    ret = Exec.Run(@"C:", "dir", "");
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with no capture and window");
    ret = Exec.Run(@"C:", "dir", "", null, false);
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with capture and no window");
    var results = Exec.GetOutput(@"C:", "dir", "");
    Console.WriteLine(results.Output.ToString());
    Console.WriteLine("Execute returned " + results.ReturnCode);
    Console.ReadLine();
    Console.WriteLine("Executing dir with real-time capture and no window");
    ret = Exec.Run(@"C:", "dir", "", ShowString);
    Console.WriteLine("Execute returned " + ret);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

The first run does not gather any output and just shows the exit code.
The second run does not gather any output but shows the window.
The effect of this that the output appears in the console window real-time.
The third run uses GetOutput to gather the output.
The effect of this is that the output does not appear until the run is completed.
The last run uses a handler to receive and display the output real-time.
In appearance this looks like the second run but it is very different.
For each line of output that is received ShowString is called.
Show string simply displays the string.
However, it could do anything it needs with the data.

I am trying to adapt the last run such that I can update a text box with the output of the command in real time. The issue that I am having is how to get it in the right context (for lack of a better term). Because OutputHandler is called asynchronously, it has to use the InvokeRequired/BeginInvoke/EndInvoke mechanism to get in sync with the UI thread. I am having a little problem with how to do this with parameters. In my code the textBox could be one of several in a tab control as several background "Run"'s could be taking place.

So far I have this:

private void btnExecute_Click(object sender, EventArgs e)
{
    // Get currently selected tab page
    var page = tcExecControl.SelectedTab;
    // Get text box (always 3rd control on the page)
    var txt = (TextBox)page.Controls[2];
    // Create string handler
    var prc = new Exec.OutputHandler((String line) =>
                  {
                      if (txt.InvokeRequired)
                          txt.Invoke(new MethodInvoker(() =>
                                     { txt.Text += line; }));
                          else txt.Text += line;
                   });
    // Command and arguments are always 1st and 2nd controls on the page
    var result = Exec.Run(@"C:", page.Controls[0].Text, page.Controls[1], prc);                              
}

But this does not seem to be working. I am not seeing any output to the txtBox.
In fact the program basically hangs in the handler.

If I change the code to use GetOutput and then write the resulting output to the text box everything works. So I know that I have the command set up properly. Using the debugger, I am able to set a break point on the "if (txt.InvokeRequired)" line and I see the first line of output coming correctly. At this point the code takes the true path of the if statement, but if I set a breakpoint on the txt.Text += line; line it never gets there.

Can anyone help me out? I'm sure I'm missing something.

解决方案

A brief description of what the code performs in this example:

The shell command (cmd.exe) is run first, using start /WAIT as parameter. More or less the same functionality as /k: the console is started without any specific task, waiting to process a command when one is sent.

StandardOutput, StandardError and StandardInput are all redirected, setting RedirectStandardOutput, RedirectStandardError and RedirectStandardInput properties of the ProcessStartInfo to true.

The console Output stream, when written to, will raise the OutputDataReceived event; it's content can be read from the e.Data member of the DataReceivedEventArgs.
StandardError will use its ErrorDataReceived event for the same purpose.
You could use a single event handler for both the events, but, after some testing, you might realize that is probably not a good idea. Having them separated avoids some weird overlapping and allows to easily tell apart errors from normal output (as a note, you can find programs that write to the error Stream instead of the output Stream).

StandardInput can be redirected assigning it to a StreamWriter stream.
Each time a string is written to the stream, the console will interpret that input as a command to be executed.

Also, the Process is instructed to rise it's Exited event upon termination, setting its EnableRaisingEvents property to true.
The Exited event is raised when the Process is closed because an Exit command is processed or calling the .Close() method (or, eventually, the .Kill() method, whcich should only be used only when a Process is not responding anymore, for some reason).

Since we need to pass the console Output to some UI controls (RichTextBoxes in this example) and the Process events are raised in a ThreadPool Thread, we must synchronize this context with the UI's.
This can be done using the Process SynchronizingObject property, setting it to the Parent Form or using the Control.BeginInvoke method, that will execute a delegate function on the thread where the control's handle belongs.
Here, a MethodInvoker representing the delegate is used for this purpose.


The core function used to instantiate the Process and set its properties and event handlers:

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

StreamWriter stdin = null;

public partial class frmCmdInOut : Form
{
    Process cmdProcess = null;
    StreamWriter stdin = null;

    public frmCmdInOut() => InitializeComponent();

    private void MainForm_Load(object sender, EventArgs e)
    {
        rtbStdIn.Multiline = false;
        rtbStdIn.SelectionIndent = 20;
    }

    private void btnStartProcess_Click(object sender, EventArgs e)
    {
        btnStartProcess.Enabled = false;
        StartCmdProcess();
        btnEndProcess.Enabled = true;
    }

    private void btnEndProcess_Click(object sender, EventArgs e)
    {
        if (stdin.BaseStream.CanWrite) {
            stdin.WriteLine("exit");
        }
        btnEndProcess.Enabled = false;
        btnStartProcess.Enabled = true;
        cmdProcess?.Close();
    }

    private void rtbStdIn_KeyPress(object sender, KeyPressEventArgs e)
    {
        if (e.KeyChar == (char)Keys.Enter) {
            if (stdin == null) {
                rtbStdErr.AppendText("Process not started" + Environment.NewLine);
                return;
            }

            e.Handled = true;
            if (stdin.BaseStream.CanWrite) {
                stdin.Write(rtbStdIn.Text + Environment.NewLine);
                stdin.WriteLine();
                // To write to a Console app, just 
                // stdin.WriteLine(rtbStdIn.Text); 
            }
            rtbStdIn.Clear();
        }
    }

    private void StartCmdProcess()
    {
        var pStartInfo = new ProcessStartInfo {
             FileName = "cmd.exe",
            // Batch File Arguments = "/C START /b /WAIT somebatch.bat",
            // Test: Arguments = "START /WAIT /K ipconfig /all",
            Arguments = "START /WAIT",
            WorkingDirectory = Environment.SystemDirectory,
            // WorkingDirectory = Application.StartupPath,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
        };

        cmdProcess = new Process {
            StartInfo = pStartInfo,
            EnableRaisingEvents = true,
            // Test without and with this
            // When SynchronizingObject is set, no need to BeginInvoke()
            //SynchronizingObject = this
        };

        cmdProcess.Start();
        cmdProcess.BeginErrorReadLine();
        cmdProcess.BeginOutputReadLine();
        stdin = cmdProcess.StandardInput;
        // stdin.AutoFlush = true;  <- already true

        cmdProcess.OutputDataReceived += (s, evt) => {
            if (evt.Data != null)
            {
                BeginInvoke(new MethodInvoker(() => {
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine);
                    rtbStdOut.ScrollToCaret();
                }));
            }
        };

        cmdProcess.ErrorDataReceived += (s, evt) => {
            if (evt.Data != null) {
                BeginInvoke(new Action(() => {
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine);
                    rtbStdErr.ScrollToCaret();
                }));
            }
        };

        cmdProcess.Exited += (s, evt) => {
            stdin?.Dispose();
            cmdProcess?.Dispose();
        };
    }
}

Since the StandardInput has beed redirected to a StreamWriter:

stdin = cmdProcess.StandardInput;

we just write to the Stream to execute a command:

stdin.WriteLine(["Command Text"]);


The sample Form can be downloaded from PasteBin.

这篇关于如何从命令中获取输出以实时显示在窗体上的控件中?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆