c# - 如何让命令的输出实时显示在窗体的控件中?

标签 c# .net winforms process

我从网络上的各种来源整理了以下代码,用于通过 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; 行上设置断点,它永远不会到达那里。

谁能帮帮我?我确定我遗漏了什么。

最佳答案

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

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

StandardOutput , StandardErrorStandardInput全部重定向,设置 RedirectStandardOutput , RedirectStandardErrorRedirectStandardInput ProcessStartInfo 的属性为 true

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

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

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

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


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

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

Console redirection in real time

示例表单可以是downloaded from PasteBin .


VB.Net版

控件名称:
rtbStdOut -> RichTextBox(蓝色背景),接收 StdOut
rtbStdErr -> RichTextBox(在中间),接收 StdErr
rtbStdIn -> RichTextBox(底部),写入 StdIn
btnStartProcess -> 按钮(右侧),启动流程
btnEndProcess -> 按钮(左侧),停止进程

Download this Form from Google Drive

Imports System.Diagnostics
Imports System.IO

Public Class frmCmdInOut

    Private cmdProcess As Process = Nothing
    Private stdin As StreamWriter = Nothing

    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        rtbStdIn.Multiline = False
        rtbStdIn.SelectionIndent = 20
    End Sub

    Private Sub btnStartProcess_Click(sender As Object, e As EventArgs) Handles btnStartProcess.Click
        btnStartProcess.Enabled = False
        StartCmdProcess(Me)
        btnEndProcess.Enabled = True

    End Sub

    Private Sub btnEndProcess_Click(sender As Object, e As EventArgs) Handles btnEndProcess.Click
        If stdin.BaseStream IsNot Nothing AndAlso stdin.BaseStream.CanWrite Then stdin.WriteLine("exit")
        btnEndProcess.Enabled = False
        btnStartProcess.Enabled = True
        cmdProcess?.Close()
    End Sub

    Private Sub rtbStdIn_KeyPress(sender As Object, e As KeyPressEventArgs) Handles rtbStdIn.KeyPress
        If e.KeyChar = ChrW(Keys.Enter) Then
            If stdin Is Nothing Then
                rtbStdErr.AppendText("Process not started" + Environment.NewLine)
                Return
            End If

            e.Handled = True
            If stdin.BaseStream.CanWrite Then
                stdin.Write(rtbStdIn.Text + Environment.NewLine)
                stdin.WriteLine() ' To write to a Console app, just stdin.WriteLine(rtbStdIn.Text); 
            End If
            rtbStdIn.Clear()
        End If
    End Sub

    Private Sub StartCmdProcess(synchObj As Control)

        ' Arguments = $"start /WAIT cscript.exe script.vbs /xpr",
        ' Batch File Arguments = "/C START /b /WAIT batchfile.bat",
        ' Test: Arguments = "START /WAIT /K ipconfig /all",

        ' start with /U
        ' StandardErrorEncoding = Encoding.Unicode,
        ' StandardOutputEncoding = Encoding.Unicode,

        Dim pStartInfo = New ProcessStartInfo() With {
            .FileName = "cmd.exe",
            .Arguments = "START /WAIT",
            .CreateNoWindow = True,
            .RedirectStandardError = True,
            .RedirectStandardInput = True,
            .RedirectStandardOutput = True,
            .UseShellExecute = False,
            .WindowStyle = ProcessWindowStyle.Hidden,
            .WorkingDirectory = Application.StartupPath
        }

        cmdProcess = New Process() With {
            .EnableRaisingEvents = True,
            .StartInfo = pStartInfo,
            .SynchronizingObject = synchObj
        }

        cmdProcess.Start()
        cmdProcess.BeginErrorReadLine()
        cmdProcess.BeginOutputReadLine()
        stdin = cmdProcess.StandardInput

        AddHandler cmdProcess.OutputDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine)
                    rtbStdOut.ScrollToCaret()
                End If
            End Sub
        AddHandler cmdProcess.ErrorDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine)
                    rtbStdErr.ScrollToCaret()
                End If
            End Sub

        AddHandler cmdProcess.Exited,
            Sub(s, evt)
                stdin?.Dispose()
                cmdProcess?.Dispose()
            End Sub
    End Sub
End Class

关于c# - 如何让命令的输出实时显示在窗体的控件中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51680382/

相关文章:

c# - 如何在 C# 中检查日期是否超过 1 年前?

.net - 在哪里可以获得 .NET 中套接字中保持事件的规范?

.net - Silverlight 与表达混合

c# - 在负十六进制和负十进制之间转换会给出错误的结果

c# - 使用 Miterjoin 围绕引导线创建线

c# - $.ajax 返回页面的 HTML 而不是结果

c# - 表达式体 VS block 体

c# - 声明一个列表并使用一个代码语句填充值

c# - 如何截取WPF控件的屏幕截图?

c# - C# winform 上的交互式谷歌地图