如何使用Win32调用在C#中关闭/打开控制台?

5
以下程序在“Console.ReadKey()”处抛出错误。
如何在禁用控制台后重新启用控制台?
    using System;
    using System.Threading;
    using System.Runtime.InteropServices;

    namespace ConsoleApplication
    {
        class Program
        {
            static void Main(string[] args)
            {
                ThreadPool.QueueUserWorkItem((o) =>
                {
                    Thread.Sleep(1000);
                    IntPtr stdin = GetStdHandle(StdHandle.Stdin);
                    CloseHandle(stdin);
                });
                Console.ReadLine();
                Console.Write("ReadLine() successfully aborted by background thread.\n");
                Console.Write("[any key to exit]");
                Console.ReadKey(); // Throws an exception "Cannot read keys when either application does not have a console or when console input has been redirected from a file. Try Console.Read."
            }

            // P/Invoke:
            private enum StdHandle { Stdin = -10, Stdout = -11, Stderr = -12 };
            [DllImport("kernel32.dll")]
            private static extern IntPtr GetStdHandle(StdHandle std);
            [DllImport("kernel32.dll")]
            private static extern bool CloseHandle(IntPtr hdl);
        }
    }
专家额外提示 如果您想知道如何在C#中终止运行ReadLine()的后台线程,这似乎是唯一的方法(thread.Abort无法工作,因为ReadLine()在操作系统的深处以非托管代码运行)。关于此话题在StackOverflow上有很多讨论,但没有人真正发现(或发布)一个令人满意的Console.ReadLine()终止方法。我认为这段代码是正确的轨迹 - 只要我们可以在禁用控制台后重新启用它。

如果你关闭了Handle...你就需要手动再次打开它。考虑到你刚刚关闭了Handle,不能保证你可以再次使用相同的Handle。你确定你关闭的是正确的Handle吗?我相信有一种更简单的方法来中止一个Readline而不是关闭标准输入的那个讨厌的Handle。 - Security Hound
@Ramhound我希望有一种更简单的方法来中止ReadLine。我已经尝试了所有方法-.Abort,向应用程序发送键(仅适用于当前焦点)等。如果有更好的方法,我全听着。 - Contango
@Ramhound 我能想到的另一种方法就是用100%托管的C#编写ReadLine的克隆版本,这样.Abort就可以工作了,但与几行Win32代码相比,这显然更加复杂。 - Contango
2个回答

6
使用PostMessage将[enter]发送到当前进程:
    class Program
    {
        [DllImport("User32.Dll", EntryPoint = "PostMessageA")]
        private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);

        const int VK_RETURN = 0x0D;
        const int WM_KEYDOWN = 0x100;

        static void Main(string[] args)
        {
            Console.Write("Switch focus to another window now to verify this works in a background process.\n");

            ThreadPool.QueueUserWorkItem((o) =>
            {
                Thread.Sleep(4000);

                var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
                PostMessage(hWnd, WM_KEYDOWN, VK_RETURN, 0);
            });

            Console.ReadLine();

            Console.Write("ReadLine() successfully aborted by background thread.\n");
            Console.Write("[any key to exit]");
            Console.ReadKey();
        }
    }

这个答案解决了调用ReadLine()时调用.Abort方法无法生效的问题,因为代码运行在深入Windows内核的非托管代码中。
与仅适用于当前进程具有焦点的答案(例如SendKeysInput Simulator)相比,此答案更优秀。
与关闭当前控制台句柄的方法相比,该答案更优秀,因为关闭当前控制台句柄将导致将来调用ReadLine()抛出错误。

我使用过这个解决方案,但如果使用CTRL + F5(Visual Studio 2017)运行项目,则无法正常工作,如果使用F5或可执行文件则可以。 - Nikita

0

不知道是否有帮助,但当我需要在Win Form应用程序中写入控制台时,我使用了这个类:

public class ConsoleHelper
{
    /// <summary>
    /// Allocates a new console for current process.
    /// </summary>
    [DllImport("kernel32.dll")]
    public static extern Boolean AllocConsole();

    /// <summary>
    /// Frees the console.
    /// </summary>
    [DllImport("kernel32.dll")]
    public static extern Boolean FreeConsole();
}

调用AllocConsole创建控制台,然后您可以向其中写入(和读取)内容。完成后,请调用FreeConsole。


1
谢谢您的建议。我已经尝试了这种方法。如果调用FreeConsole()然后AllocConsole(),任何现有的ReadLine()调用都会抛出异常"Attempted to read or write protected memory. This is often an indication that other memory is corrupt."。在C#中,这个异常是无法通过try/catch捕获的。而且还有一个比较坏的副作用:它会失去控制台里的所有历史记录,因为实际上相当于开启了一个全新的控制台。我不知道是否有其他方法? - Contango

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