将 WPF Windows 应用程序中的输出 Console.WriteLine 输出到实际控制台

29

背景:我正在努力为现有的WPF Windows应用程序添加命令行和批处理功能。当我在启动时检测到某些选项时,我会抑制窗口出现,进行一些处理并立即退出。现在,因为没有UI界面,我想要将一些消息输出到stdout/stderr。请考虑以下代码:

namespace WpfConsoleTest
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            Console.WriteLine("Start");
            System.Threading.Thread.Sleep(1000);
            Console.WriteLine("Stop");
            Shutdown(0);
        }
    }
}

当我在命令行中运行时,我期望看到以下输出:

Start
Stop

相反:

C:\test>WpfConsoleTest.exe

C:\test>

你可以重定向输出:

C:\test>WpfConsoleTest.exe > out.txt

C:\test>type out.txt
Start
Stop

遗憾的是,重定向到CON无法正常工作:

C:\test>WpfConsoleTest.exe > CON

C:\test>

另一个问题是WpfConsoleTest.exe在启动后立即退出。因此:

C:\test>WpfConsoleTest.exe > out.txt & type out.txt

C:\test>

但是:

C:\test>WpfConsoleTest.exe > out.txt & ping localhost > nul & type out.txt
Start
Stop

到目前为止,我能想到的最佳解决方案是使用start /B /wait
C:\test>start /B /wait WpfConsoleTest.exe > out.txt & type out.txt
Start
Stop

这种方法大多数情况下是可行的,如果将它包裹在一个批处理文件中,您可以保留错误代码等信息。但唯一的缺点是,应用程序结束后输出到终端,即无法跟踪进度,必须等待正在进行的操作完成。

因此,我的问题是:如何从WPF Windows应用程序输出到父控制台? 此外,为什么很难从WPF获取stdout / stderr?

我知道可以在项目设置中将应用程序类型更改为控制台应用程序,但这会有一个不好的副作用-即使您只是双击exe,控制台窗口也会一直显示。 这个解决方案 也不可行,因为它即使从cmd运行应用程序也会创建新的控制台。

编辑:为了澄清,我希望我的应用程序将输出到现有的控制台,如果存在的话,并且不要在缺少控制台时创建新的控制台。


类似问题:https://dev59.com/1nVC5IYBdhLWcg3w21Iq - Lukasz Madon
2
这个问题的链接已经在我的问题的最后一段中了。 - gwiazdorrr
6个回答

27

经过一番搜索,我找到了这个答案。现在的代码是:

namespace WpfConsoleTest
{
    public partial class App : Application
    {
        [DllImport("Kernel32.dll")]
        public static extern bool AttachConsole(int processId);

        protected override void OnStartup(StartupEventArgs e)
        {
            AttachConsole(-1);
            Console.WriteLine("Start");
            System.Threading.Thread.Sleep(1000);
            Console.WriteLine("Stop");
            Shutdown(0);
        }
    }
}

直接调用exe仍然会产生一个不好的副作用,与调用立即返回有关:

C:\test>WpfConsoleTest.exe

C:\test>Start
Stop

^^^^
The cursor will stay here waiting for the user to press enter!

解决方案是,再次使用start

C:\test>start /wait WpfConsoleTest.exe
Start
Stop

感谢您的输入!


谢谢,gwiazdorrr。这可能是更好的解决方案。但我仍然不满意它带来的不良影响。也许你找到了更好的解决方案? - Boris Modylevsky
@BorisModylevsky:恐怕不行,我已经使用批处理脚本调用了“start”版本。 - gwiazdorrr
1
非常好,比将应用程序更改为控制台并在启动时隐藏控制台窗口要好得多(这是一种可怕的体验,因为控制台窗口会在 UI 打开之前短暂出现)。 - Aviad Ezra
1
AttachConsole存在的问题是,当应用程序完成后,用户必须点击“Enter”才能继续使用控制台(看起来像是挂起)。您可以在结尾处调用SendKeys.SendWait("{ENTER}");以释放控制台。 - Aviad Ezra
非常高兴我找到了这个,之前遇到了一些极其复杂的解决方案。对于我来说,这个方法完美地解决了从C# WPF Windows应用程序获取标准输出的问题。控制台在结尾等待输入并不影响我的使用情况,因为我是从另一个WPF应用程序启动WPF应用程序,并将标准输出重定向到另一个应用程序。 - Richard Moore
显示剩余2条评论

18

过去我的做法是将它制作成控制台应用程序,并调用P/Invoke来隐藏应用的相关控制台窗口,一旦我显示了WPF窗口:

//added using statements per comments...
using System.Diagnostics;
using System.Runtime.InteropServices;

    internal sealed class Win32
    {
        [DllImport("user32.dll")]
        static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        public static void HideConsoleWindow()
        {
            IntPtr hWnd = Process.GetCurrentProcess().MainWindowHandle;

            if (hWnd != IntPtr.Zero)
            {
                ShowWindow(hWnd, 0); // 0 = SW_HIDE
            }
        }
    }

这是目前最好的解决方案。我只是不喜欢在窗口显示之前出现控制台窗口,我认为这可能会对普通用户造成干扰和困惑。 - gwiazdorrr
3
很棒的解决方案,虽然我认为我同意@gwiazdorrr的观点,但对于实用项目,我认为这是可以接受的……只是不确定纯粹的“生产”项目是否适用。 - Richard B
1
为了让代码编译通过并且想知道这些命令所在的命名空间,可以添加 using System.Diagnostics;using System.Runtime.InteropServices; - SharpC
下面的解决方案更好。让它成为一个窗口应用并在启动时附加控制台。这样控制台窗口就不会在界面之前短暂地出现(看起来不专业)。 - Aviad Ezra
如果我选择这个方案,PowerShell 将不会显示任何输出。 - Leo Li

8

默认情况下,WPF应用程序没有控制台,但是您可以轻松地为其创建一个,并像在控制台应用程序中一样写入它。我以前使用过这个辅助类:

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

要在你的示例中使用它,你应该能够这样做:
namespace WpfConsoleTest
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            ConsoleHelper.AllocConsole(); 
            Console.WriteLine("Start");
            System.Threading.Thread.Sleep(1000);
            Console.WriteLine("Stop");
            ConsoleHelper.FreeConsole();
            Shutdown(0);
        }
    }
}

现在,如果你从命令行运行它,你应该能够在控制台看到“Start”和“Stop”。


1
是的,但是我看到的将是一个新的控制台窗口,即使我从命令行启动它。我想在现有控制台中看到输出。 - gwiazdorrr
1
嗯...在WinForms中对我来说运行良好,但似乎WPF做了一些不同的事情。你可以尝试使用AttachConsole(http://msdn.microsoft.com/en-us/library/windows/desktop/ms681952%28v=vs.85%29.aspx),但你需要获取启动应用程序的进程(可能是cmd.exe)的句柄。 - Matt Burland
非常好用。只是对于一些新手来说,这是所需的代码...protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); ConsoleHelper.AllocConsole(); }protected override void OnExit(ExitEventArgs e) { ConsoleHelper.FreeConsole(); base.OnExit(e); } - Eric Ouellet
2
这个提议并不是解决方案。在这里,我们总是使用AllocConsole创建一个新的控制台,无论我们是否已经打开了一个。这意味着当您从命令行启动WPF应用程序时,仍然无法将console.writeline写入命令行,并且在程序结束时,所有消息都会消失(因为额外的控制台已关闭)。 - codingdave
使用这个解决方案,我无法将输出重定向到文件,我的意思是,如果我调用start > result.txt,我将在控制台上直接看到结果,但在result.txt中什么也看不到。 还有其他的想法吗? - Leo Li
显示剩余3条评论

0

到目前为止,如果我想要实现以下功能:

  1. 能够将输出重定向到其他输出流,例如使用命令行中的">"来将输出重定向到文件
  2. 支持PowerShell输出

我还没有找到最佳解决方案。

  1. 因此,也许正确的答案是:
  2. 将GUI应用程序与控制台应用程序分开,例如,GUI称为Tool.exe,控制台应用程序称为ToolConsole.exe。
  3. 在VS中将Tool.exe类型标记为“Windows应用程序”,而将ToolConsole.exe标记为“控制台应用程序”。
  4. 放置一个名为“Tool.bat”的批处理文件,它可以包含以下内容:
ToolConsole.exe %1 %2 %3 %4
  1. 当您调用 Tool Param1 Param2... 时,实际上正在调用 ToolConsole.exe。而当您通过双击直接运行 Tool.exe 时,则使用的是 Tool.exe 的 GUI。

这是我的解决方案,如果有人有解决我提到的所有情况的想法,我很乐意听取。


0
从@gwiazdorrr和@MattBurland的答案中改编,转换为VB.NET。展示如何处理与Visual Studio相关的调试问题。
' VB
''' <summary>Get the process ID.</summary>
Public Declare Function GetCurrentProcessId Lib "kernel32" () As Integer

''' <summary>Allocates a new console for current process.</summary>
Public Declare Function AttachConsole Lib "kernel32" (processID As Integer) As Boolean

''' <summary>Frees the console.</summary>
Public Declare Function FreeConsole Lib "kernel32" () As Boolean

''' <summary>Get a list of the process IDs currently attached to the console.</summary>
Public Declare Function GetConsoleProcessList Lib "kernel32" (processList As UInt32(), processCount As UInt32) As UInt32

Private Sub ShowCommandLine()
    ' Attach a console to this WPF application
    AttachConsole(-1)

    ' Output will not be visible if debugging from Visual Studio. Check whether a console is attached and
    ' that a debugger is not attached.
    Dim processList As UInt32()
    ReDim processList(64)
    Dim processesAttachedToConole As UInt32 = GetConsoleProcessList(processList, 64)
    Dim debuggerAttached As Boolean = System.Diagnostics.Debugger.IsAttached
    If (processesAttachedToConole = 0) OrElse (debuggerAttached) Then
        ' Don't display anything on the console window
        ' You could pop up a WPF message here...
    Else
        ' Show help in the console window
        Console.WriteLine()

        ' Version information (by way of example)
        Dim myAssembly As Assembly = Assembly.GetEntryAssembly()
        Dim myName As String = myAssembly.GetName().Name
        Dim myVersion As FileVersionInfo = FileVersionInfo.GetVersionInfo(myAssembly.Location)
        Console.WriteLine(String.Format("{0} v{1}", myName, myVersion.FileVersion))
        Console.WriteLine()

        ' Force a RETURN keypress, otherwise the user must hit RETURN themselves
        SendKeys.SendWait("{ENTER}")
    End If

    ' Detach the console
    FreeConsole()
End Sub

-1
在项目属性中,您可以将项目类型从Windows应用程序更改为控制台应用程序,据我所知,唯一的区别是您可以在整个应用程序的生命周期中使用控制台。不过我还没有尝试过在WPF中使用它。
如果您想打开和关闭控制台,您应该使用Alloc/FreeConsole,但通常更简单的方法是更改项目类型。

正如我所写的,将其更改为控制台应用程序将创建一个控制台窗口,如果您双击exe。 我想要的是将输出打印到现有控制台中,如果没有,请不要创建新的控制台。 - gwiazdorrr

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