如何向进程发送按键而不是字符?

7

System.Diagnostics.Process公开了一个名为StandardInput的StreamWriter,据我所知,它只接受字符。

但我需要发送按键,而有些按键不能很好地映射到字符。

我该怎么办?


例如,哪些按键? - Rick Mogstad
1
这是Unix管道抽象中的终极泄漏。重定向通过字符流进行,它无法模拟非键入按键。 - Hans Passant
你是想捕获并转发到另一个进程,还是仅仅发送非可打印字符作为控制手段等? - Marc Bollinger
@Marc 我正在尝试为类似于octave.exe的交互式脚本解释器创建GUI界面。 - Jader Dias
请查看 http://stackoverflow.com/search?q=autohotkey - lsalamon
请查看这个问题:https://dev59.com/yXVC5IYBdhLWcg3weA8- - KindDragon
4个回答

30

您混淆了输入流和控制信号。 控制台进程有一个默认的输入流,您可以使用StandardInput控制,正如您已经知道的那样。 但是Ctrl-C和Ctrl-Break不是通过此流发送到进程的字符,而是进程使用注册的信号处理程序接收到的控制信号,请参见CTRL+C and CTRL+BREAK Signals:

默认情况下,当控制台窗口具有键盘焦点时,CTRL+C或CTRL+BREAK被视为信号(SIGINT或SIGBREAK),而不是键盘输入。

要向进程发送虚假信号,可以使用GenerateConsoleCtrlEvent并发送CTRL_C_EVENTCTRL_BREAK_EVENT。 这个API没有.NET等效项,所以您必须PInvoke它。

要从.NET中使用它,您只需要包含函数定义:

const int CTRL_C_EVENT = 0;
const int CTRL_BREAK_EVENT = 1;

[DllImport("kernel32.dll")]
static extern bool GenerateConsoleCtrlEvent(
    uint dwCtrlEvent,
    uint dwProcessGroupId);

http://www.google.com/codesearch/p?hl=en#ncfzeHH4QLA/pubs/consoledotnet/consoledotnet.zip%7CYrqh4ujA6zA/ConsoleDotNet/WinCon.cs - Jader Dias
我正在使用FFmpeg进行屏幕录制。这对我有用吗?实际上,我想停止使用FFmpeg开始屏幕录制的过程。 - Ahmad

6
这里有一个输入模拟器,可以在Codeplex找到,可能对你有帮助。我正在编写一个示例代码,并将很快发布在这里,请注意,输入模拟器类似于Remus提供的链接中发现的内容...
编辑:我发现它有限制,您可以使用典型的System.Windows.Forms.SendKeys.Send方法,它确实有效!但是,该进程必须满足以下条件:
- 没有流的重定向 - 不能是隐藏窗口(因为它将失败,因为窗口句柄不可见,无法将其置于前景使其活跃!) - 必须显示进程窗口才能有效!
在您的情况下,需要查找窗口,通过pinvoke'SetForegroundWindow'将其设置为活动状态,并发送序列^{BREAK},这将向进程发送Ctrl+Break信号,这很有效(特别是如果进程是命令行程序/批处理文件)。这里是CodeProject上的一篇文章,它确切地完成了这个,而且与SendKeys非常相似...我还没有把代码粘贴到这里来演示......
编辑#2:实际上,我感到非常惊讶......因为这段代码将显示(概念验证):
- InputSimulator(如前所述) - 一个Windows表单,其中包含一个按钮,当窗体加载时,它自动运行类。单击按钮时,它会向隐藏进程发送ctrl-break - 输出流确实被重定向并且是一个隐藏的窗口。 - 奇怪的是,输出被捕获但不会在调试窗口中实时显示结果,它是缓冲的(我猜),直到进程终止,整个输出被显示...... - 在FindWindow API调用中,我有点作弊,因为我知道窗口的标题是什么,并且以某种方式把它置于前景,并使用InputSimulator将按键发送给它...或者使用传统的旧的SendKeys函数...我在Thread.Sleep中使用的原因是确保按键以特定顺序“推入“'隐藏的'活动前景窗口的键盘队列中,尽管如此,窗口是隐藏的' - 我使用了“netstat -e 5”命令来永远循环,每5秒刷新一次结果,直到收到Ctrl+C打破无限循环。
public partial class Form1 : Form
{
    private TestNetStat netStat = new TestNetStat();
    public Form1()
    {
       InitializeComponent();
       using (BackgroundWorker bgWorker = new BackgroundWorker())
       {
           bgWorker.DoWork += new DoWorkEventHandler(bgWorker_DoWork);
           bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgWorker_RunWorkerCompleted);
           bgWorker.RunWorkerAsync();
       }
    }

    void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
       System.Diagnostics.Debug.WriteLine("BGWORKER ENDED!");
    }

    private void  bgWorker_DoWork(object sender, DoWorkEventArgs e)
    {
       netStat.Run();
    } 
    void btnPost_Click(object sender, EventArgs e)
    {
       netStat.PostCtrlC();
       System.Diagnostics.Debug.WriteLine(string.Format("[{0}] - {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), this.netStat.OutputData.Replace(Environment.NewLine, "")));
    }
}

public class TestNetStat
{
    private StringBuilder sbRedirectedOutput = new StringBuilder();
    //
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    [DllImport("user32")]
    public static extern int SetForegroundWindow(IntPtr hwnd);
    public string OutputData
    {
       get { return this.sbRedirectedOutput.ToString(); }
    }
    public void PostCtrlC()
    {
       IntPtr ptr = FindWindow(null, @"C:\Windows\System32\netstat.exe");
       if (ptr != null)
       {
          SetForegroundWindow(ptr);
          Thread.Sleep(1000);
          WindowsInput.InputSimulator.SimulateModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.CANCEL);
          // SendKeys.Send("^{BREAK}");
          Thread.Sleep(1000);
        }
    }
    public void Run()
    {
        System.Diagnostics.ProcessStartInfo ps = new System.Diagnostics.ProcessStartInfo();
        ps.FileName = "netstat";
        ps.ErrorDialog = false;
        ps.Arguments = "-e 5";
        ps.CreateNoWindow = true;
        ps.UseShellExecute = false;
        ps.RedirectStandardOutput = true;
        ps.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
        using (System.Diagnostics.Process proc = new System.Diagnostics.Process())
        {
           proc.StartInfo = ps;
           proc.EnableRaisingEvents = true;
           proc.Exited += new EventHandler(proc_Exited);
           proc.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(proc_OutputDataReceived);
           proc.Start();
           proc.BeginOutputReadLine();
           proc.WaitForExit();
        }
     }

     void proc_Exited(object sender, EventArgs e)
     {
        System.Diagnostics.Debug.WriteLine("proc_Exited: Process Ended");
     }

     void proc_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
     {
         if (e.Data != null)
         {
            this.sbRedirectedOutput.Append(e.Data + Environment.NewLine);
            System.Diagnostics.Debug.WriteLine("proc_OutputDataReceived: Data: " + e.Data);
         }
     }
}

除了一些小问题,我知道netStat是在“BackgroundWorker”线程上运行的,并且我直接从主GUI线程调用了“PostCtrlC”方法...这只是一个概念验证代码,但它确实表明它需要实现“ISynchronizeInvoke”使其线程安全,但是...它确实可以工作。


但是,如果您正在运行使用netstat.exe的多个进程,那么使用PostCtrlC()将会影响所有进程吗?如果您知道进程SessionId,是否有一种相同的方法来执行相同的操作? - Constantin
@Constantine 该代码仅针对运行 netstat.exe 的一个窗口。你可以使用 EnumWindows 并创建回调函数 EnumWindowsProc 来获取句柄并检查它,看看该句柄的标题是否包含 netstat.exe...如果找到了,则将其带到前台,并像上面的概念验证代码一样“zap”掉 :) - t0mm13b

3
你看过这个很棒的工具吗 - AutoIt。这是一个脚本工具。要发送退格键,你可以使用Send("{BACKSPACE}")
这是一个很棒的工具,可以帮助自动化许多手动点击/双击等操作。
这和你的问题相关吗?

嗨,这很相关,但我更喜欢一种可以直接在.NET中使用的解决方案。 - Jader Dias
1
@Jader;AutoIt有一个可用的dll,你可以轻松地将其添加到你的应用程序中... - Jerod Venema

0

如果您有一个可以发送按键的 Windows Forms 窗口,那么 SendKeys 可能是一个适当的解决方案。

对于按下回退和 Ctrl+C,应该是

SendKeys.Send("{BACKSPACE}^C");

发送Ctrl+C并不能在进程中起作用,应该使用^{BREAK}... - t0mm13b

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接