为什么关闭使用AllocConsole启动的控制台会导致整个应用程序退出?我能改变这种行为吗?

30
我希望控制台窗口只是消失,或者更好的是隐藏,但我想让我的应用程序继续运行。这可行吗?我想使用Console.WriteLine并将控制台作为输出窗口。我想能够隐藏和显示它,并且不想因为关闭控制台而导致整个应用程序死亡。

编辑

代码:

internal class SomeClass {

    [DllImport("kernel32")]
    private static extern bool AllocConsole();

    private static void Main() {
        AllocConsole();
        while(true) continue;
    }
}

编辑2

我尝试了此处[Capture console exit C#]的已接受解决方案,根据这个问题的评论建议。示例代码存在一个错误,即DLLImport需要是“kernel32.dll”或“kernel32”,而不是“Kernel32”。更改后,当我点击控制台窗口上的X时,我的处理程序会收到关于CTRL_CLOSE_EVENT的消息。然而,调用FreeConsole和/或返回true并不能阻止应用程序终止。


@DanielHilgarth 整个重点是拥有一个控制台应用程序,而不向运行该应用程序的用户显示控制台。 它可以是程序内运行的任何代码。 - Servy
1
我同意Daniel的观点,我们需要查看关键代码行以重现此问题。就我理解的问题,这不应该发生。调用FreeConsole不应该结束整个应用程序/进程。您应该能够调用FreeConsole,然后稍后调用AllocConsoleAttachConsole - Cody Gray
@Servy 或者也可以。无论哪种情况,他们都需要捕获该事件,以便用户关闭控制台窗口时不会使应用程序崩溃。 (当然,他们可能已经在尝试这样做了 - 在这种情况下,我同意Daniel说我们需要更多有关实际代码的信息)。 - Jon Hanna
1
好的,在第二次编辑之后,我至少知道一件事:我不知道答案!我也有点担心,因为虽然我从来没有做过这个,但我一直认为如果必须的话,我可以在那个事件的响应中释放,所以我真的希望有一个答案。 - Jon Hanna
1
制作用于配置字体、颜色等选项窗口,并将其与文本框连接起来需要相当大的努力。Windows控制台子系统已经存在。如果我找不到现成的解决方案,我肯定会采取这种方法,但这似乎有点傻。 - Kelsie
显示剩余5条评论
4个回答

30
啊,是的,这是使用Windows控制台子系统的一个注意事项。当用户关闭控制台窗口(无论如何分配控制台),所有附加到控制台的进程都将终止。对于控制台应用程序(即专门针对控制台子系统而非标准Windows应用程序的应用程序),这种行为显然有意义,但在像你这样的情况下可能会带来很大的麻烦。
我所知道的唯一解决方法是使用SetConsoleCtrlHandler函数,它允许您注册处理器函数以处理Ctrl+CCtrl+Break信号以及系统事件,例如用户关闭控制台窗口、用户注销或系统关闭。文档说,如果您只想忽略这些事件,可以将第一个参数设置为null。例如:
[DllImport("kernel32")]
static extern bool SetConsoleCtrlHandler(HandlerRoutine HandlerRoutine, bool Add);

delegate bool HandlerRoutine(uint dwControlType);

static void Main()
{
    AllocConsole();
    SetConsoleCtrlHandler(null, true);
    while (true) continue;
}
那对于Ctrl+CCtrl+Break信号,这个方法完美地解决了问题(否则应用程序也会终止),但它却无法解决您所问的那个问题,即用户关闭控制台窗口时系统生成的CTRL_CLOSE_EVENT

说实话,我不知道该如何防止这种情况。即使在SDK示例中也不能忽略CTRL_CLOSE_EVENT。我在一个小测试应用程序中尝试过,当你关闭窗口并打印消息时,它会哔哔作响,但进程仍会被终止。

更令人担忧的是,文档让我觉得这是不可能的:

当用户关闭控制台、注销或关闭系统时,系统会生成CTRL_CLOSE_EVENTCTRL_LOGOFF_EVENTCTRL_SHUTDOWN_EVENT信号,以便进程在终止之前有机会清理。控制台函数或调用控制台函数的任何C运行时函数可能在处理前面提到的三个信号期间无法可靠工作。原因是在执行进程信号处理程序之前,一些或所有内部控制台清理例程可能已经被调用。

正是那最后一句话引起了我的注意。如果控制台子系统在用户试图关闭窗口时立即开始清理自己,那么事后阻止它可能是不可能的。

(至少现在你理解了问题,也许其他人能提供解决方案!)


1
不过似乎还有一种方法。例如,Far Manager仍然可以在我关闭控制台窗口时,在编辑器中提示保存。它的代码是开源的,因此比我更懂C ++的人可能能够了解他们如何做到这一点。 - Joey
1
说实话,我不知道如何防止这种情况发生。 你能否在回调函数中欺骗并调用FreeConsole,使得控制台不再与该进程关联,并且当回调函数返回时,没有进程需要终止? - Dwayne Robinson

25

很遗憾,你无法真正改变这种行为。

控制台窗口是“特殊”的,因为它们由另一个进程托管,并且不允许子类化。这限制了您修改其行为的能力。

据我所知,你有两个选择:

1. 完全禁用关闭按钮。您可以使用以下代码片段实现:

HWND hwnd = ::GetConsoleWindow();
if (hwnd != NULL)
{
   HMENU hMenu = ::GetSystemMenu(hwnd, FALSE);
   if (hMenu != NULL) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
}

2. 放弃使用控制台,并实现自己的文本输出方案。

第二个选项更加复杂,但提供了最大的控制权。我在 CodeProject 上找到了一篇文章,其中使用富文本编辑控件来显示文本(富文本编辑控件具有类似控制台的流式文本功能,因此非常适合这种应用程序)。


很遗憾,您的解决方案(#1)无法处理使用Alt + F4的情况,而且显然没有办法子类化窗口以抑制WM_SYSKEYDOWN / WM_SYSKEYUP以解决此问题。 - Mike Weir

11

当使用AllocConsoleAttachConsole打开控制台窗口后,关闭该窗口将导致关联的进程退出。无法逃脱这种情况。

在Windows Vista之前,关闭控制台窗口会向用户显示一个确认对话框,询问是否终止该进程,但是Windows Vista及更高版本没有提供任何此类对话框,进程将被终止。

解决此问题的一种方法是避免使用 AttachConsole,并通过其他方式实现所需功能。

例如,在OP所描述的情况下,需要使用Console静态类在控制台上输出一些文本。

可以很容易地通过进程间通信实现此目的。例如,可以开发一个控制台应用程序作为回声服务器。

namespace EchoServer
{
    public class PipeServer
    {
        public static void Main()
        {
            var pipeServer = new NamedPipeServerStream(@"Com.MyDomain.EchoServer.PipeServer", PipeDirection.In);
            pipeServer.WaitForConnection();

            StreamReader reader = new StreamReader(pipeServer);

            try
            {
                int i = 0;
                while (i >= 0)
                {
                    i = reader.Read();
                    if (i >= 0)
                    {
                        Console.Write(Convert.ToChar(i));
                    }
                }
            }
            catch (IOException)
            {
                //error handling code here
            }
            finally
            {
                pipeServer.Close();
            }
        }
    }
} 

然后,不要为当前应用程序分配或附加控制台,可以从应用程序内部启动回显服务器,并将Console的输出流重定向以写入管道服务器。

class Program
{
    private static NamedPipeClientStream _pipeClient;

    static void Main(string[] args)
    {
        //Current application is a Win32 application without any console window
        var processStartInfo = new ProcessStartInfo("echoserver.exe");

        Process serverProcess = new Process {StartInfo = processStartInfo};
        serverProcess.Start();

        _pipeClient = new NamedPipeClientStream(".", @"Com.MyDomain.EchoServer.PipeServer", PipeDirection.Out, PipeOptions.None);
        _pipeClient.Connect();
        StreamWriter writer = new StreamWriter(_pipeClient) {AutoFlush = true};
        Console.SetOut(writer);

        Console.WriteLine("Testing");

        //Do rest of the work. 
        //Also detect that the server has terminated (serverProcess.HasExited) and then close the _pipeClient
        //Also remember to terminate the server process when current process exits, serverProcess.Kill();
        while (true)
            continue;
    }
}

这只是可能的解决方案之一。实质上,解决方法是将控制台窗口分配给它自己的进程,以便可以终止而不影响父进程。


这似乎是一个可行的解决方案。除非楼主说有另一个答案更直接地帮助了她的问题,否则我会给这个答案提供悬赏。 - Jon Hanna
1
我最终创建了一个自定义表单,在其自己的线程上运行消息循环。它有一个RichTextBox和其他一些东西。看起来工作得相当不错。 - Kelsie
@Kelsie 这就像完全避免控制台,偏离了原始问题陈述的方向 :) - Amit Mittal
是的,没错。我只是想把它作为可能解决问题的一个解决方案发布出来。感谢您的建议。 - Kelsie

-2

虽然此链接可能回答了问题,但最好在此处包含答案的必要部分并提供链接以供参考。仅限链接的答案如果链接页面更改,则可能无效。-【来自审查】 - acarlstein
这根本没有回答问题。问题是关于如何编写软件以特定的方式运行。我不会发布需要用户运行外部代理才能使其正常运行的软件。 - Kelsie

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