如何从命令中获取输出以实时显示在窗体上的控件中?

Posted

技术标签:

【中文标题】如何从命令中获取输出以实时显示在窗体上的控件中?【英文标题】:How do I get output from a command to appear in a control on a Form in real-time? 【发布时间】:2019-01-11 19:19:16 【问题描述】:

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

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;

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

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);

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

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

到目前为止,我有这个:

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);                              

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

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

谁能帮帮我?我确定我错过了什么。

【问题讨论】:

在 txt.Text += 行设置断点时;您是在运行代码还是单步执行?如果是第一次,那么它可能在第一次实际启动之前第二次调用该方法并在那里导致问题。只是尝试一下。 我很确定我尝试了两种方式...继续并逐步。 尝试将整个 Exec.OutputHandler 代码放入它自己的函数中,看看是否有帮助。换行: txt.Text += line; ));改为调用您创建的函数。 你的意思是something like this?您可以使用进程事件(包括Exited 事件,启用设置EnableRaisingEvents = true;),抛弃WaitForExit 并将StandardInput 重定向到StreamWriter,当您可以编写命令时。如果您需要示例代码,请告诉我。 @Jimi ...我绝对可以使用示例代码。 【参考方案1】:

简要说明此示例中代码的作用:

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

StandardOutputStandardErrorStandardInput 都被重定向,将ProcessStartInfo 的RedirectStandardOutput、RedirectStandardError 和RedirectStandardInput 属性设置为true

控制台输出流在写入时会引发OutputDataReceived 事件;它的内容可以从DataReceivedEventArgs 的e.Data 成员中读取。StandardError 将使用其ErrorDataReceived 事件用于相同目的。 您可以为这两个事件使用一个事件处理程序,但是,经过一些测试,您可能会意识到这可能不是一个好主意。将它们分开可以避免一些奇怪的重叠,并可以轻松地将错误与正常输出区分开来(请注意,您可以找到写入错误流而不是输出流的程序)。

StandardInput 可以重定向到StreamWriter 流。 每次将字符串写入流时,控制台都会将该输入解释为要执行的命令。

此外,进程被指示在终止时引发其Exited 事件,将其EnableRaisingEvents 属性设置为trueExited 事件在 Process 关闭时引发,因为处理了 Exit 命令或调用了 .Close() 方法(或者,最终是 .Kill() 方法,仅应在 Process 不再响应时使用, 出于某种原因)。

由于我们需要将控制台输出传递给一些 UI 控件(在此示例中为 RichTextBoxes)并且在 ThreadPool 线程中引发 Process 事件,因此我们必须将此上下文与 UI 同步。 这可以使用 Process SynchronizingObject 属性,将其设置为父窗体或使用 Control.BeginInvoke 方法来完成,这将在控件句柄所属的线程上执行委托函数。 在这里,代表委托的MethodInvoker 用于此目的。


用于实例化进程并设置其属性和事件处理程序的核心函数:

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

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();
        ;
    

由于 StandardInput 已被重定向到 StreamWriter:

stdin = cmdProcess.StandardInput;

我们只是写入 Stream 来执行命令:

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

示例表单可以是downloaded from PasteBin。

【讨论】:

太棒了!我将不得不尝试一下,看看我是否可以让它为我工作。 @Cyber​​clops 如果您需要帮助在某些特定上下文中实现此代码,请询问。 我正在尝试组合一个类,我可以在这种情况下重用它。我希望能够在没有输出的情况下运行并且只获取返回代码,能够运行并获取返回代码和返回给调用者的输出,并且能够传入可以处理实际输出的处理程序 -时间。 对于第一种情况,您已经有了一个实用的解决方案。第二个取决于外部过程。使用cmd.exe,您拥有所需的一切。如果您必须与拥有自己的环境netshftptelnet等)的进程进行交互,您可能希望直接执行它们。我不确定你在最后一部分指的是什么。无论如何,这是理论。如果您有一些特定的编码问题,请发布另一个特定问题,也许让我知道(在此处发布链接)。我对这件事有很多无用的想法:) 对于最后一部分,我的意思是我希望处理来自 stdout/stderr 的输出的代码能够调用用户提供的处理程序来处理输出。您提供的代码示例对于我的最终目的来说不够通用。该代码使用对象的硬编码项来接收输出 rtbStdOut 和 rtbStdErr,并将 SyncronizingObject 的设置硬编码为 this。对于通用解决方案,这些必须由调用者提供。我尝试以这种方式重新编码您的示例代码,但无法使其正常工作。

以上是关于如何从命令中获取输出以实时显示在窗体上的控件中?的主要内容,如果未能解决你的问题,请参考以下文章

C#如何在一个窗体显示另一个窗体的文本内容

winform中如何获取控件在窗体上位置?

vb中如何查询一个窗体上的控件类名

VBA迭代主窗体上的控件,同时忽略子窗体

Delphi_子窗体继承父窗体后如何显示父窗体上的控件

delphi 如何通过单击一个窗体的按钮给一个frame的控件赋值