如何创建一个能够自行决定显示为控制台或窗口应用程序的C#应用程序?

49

有没有办法启动一个带有以下功能的C#应用程序?

  1. 它通过命令行参数确定是窗口化还是控制台应用程序
  2. 当被要求窗口化时,它不显示控制台,并且在从控制台运行时不显示GUI窗口。

例如,

myapp.exe /help
会输出到你使用的控制台上的stdout,但只有
myapp.exe
将启动我的Winforms或WPF用户界面。

目前我所知道的最好的答案涉及拥有两个独立的exe并使用IPC,但那感觉非常令人生气。


为了获得上述示例中描述的行为,我有哪些选项和权衡?我也可以接受特定于Winform或WPF的想法。

11个回答

58

将应用程序制作为常规Windows应用程序,如果需要,动态创建控制台。

更多细节请参见此链接(下面的代码来自那里)。

using System;
using System.Windows.Forms;

namespace WindowsApplication1 {
  static class Program {
    [STAThread]
    static void Main(string[] args) {
      if (args.Length > 0) {
        // Command line given, display console
        if ( !AttachConsole(-1) ) { // Attach to an parent process console
           AllocConsole(); // Alloc a new console
        }

        ConsoleMain(args);
      }
      else {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
      }
    }
    private static void ConsoleMain(string[] args) {
      Console.WriteLine("Command line = {0}", Environment.CommandLine);
      for (int ix = 0; ix < args.Length; ++ix)
        Console.WriteLine("Argument{0} = {1}", ix + 1, args[ix]);
      Console.ReadLine();
    }

    [System.Runtime.InteropServices.DllImport("kernel32.dll")]
    private static extern bool AllocConsole();

    [System.Runtime.InteropServices.DllImport("kernel32.dll")]
    private static extern bool AttachConsole(int pid);

  }
}

1
这会创建一个新的控制台窗口吗?我认为最初的问题是当用户键入MyApp.exe /help时,将输出到“您使用的控制台”。 - MichaC
你是对的 - 要附加到现有控制台,你需要使用AttachConsole(-1)。更新代码以反映这一点。 - Eric Petroelje
11
这种方法最大的问题似乎是当你从shell中启动时,进程会立即返回到启动控制台。在这种情况下,stdout会开始覆盖其他文本,因为你正在使用该shell/console。如果有一种方法可以避免这种情况,那将非常方便,但我还没有找到任何解决方法。 - Matthew
大多数WinForm应用程序都有一个选项,当您关闭主窗体时关闭可执行文件。如果您遵循使用主窗体的惯例,您可以在主窗体上执行.ShowDialog,这样当它关闭时,整个应用程序也会关闭。下面的答案中有一个示例。 - Richard R

17

我基本上按照Eric的回答所描述的方式来处理,此外我使用FreeConsole分离控制台,并使用SendKeys命令将命令提示符恢复。

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

    [DllImport("kernel32.dll")]
    private static extern bool AttachConsole(int pid);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool FreeConsole();

    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length > 0 && (args[0].Equals("/?") || args[0].Equals("/help", StringComparison.OrdinalIgnoreCase)))
        {
            // get console output
            bool attachedToConsole = AttachConsole(-1);
            if (!attachedToConsole)
                AllocConsole();

            ShowHelp(); // show help output with Console.WriteLine
            FreeConsole(); // detach console

            if (attachedToConsole)
            {
                // get command prompt back
                System.Windows.Forms.SendKeys.SendWait("{ENTER}"); 
            }

            return;
        }

        // normal winforms code
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MainForm());
    }

5
这应该是答案。SendKeys 是重要的。 - derFunk
是的,使用ENTER键发送按键信息很重要,但只有在AttachConsole成功后才需要这样做,也就是说存在父控制台。如果你的.exe文件是从资源管理器中双击打开的,当它发送Enter键时,如果资源管理器窗口仍然处于焦点状态,则会生成另一个应用程序实例。存储AttachConsole的结果,并使用它来决定是否调用SendKeys。 - demoncodemonkey

7

编写两个应用程序(一个控制台,一个Windows),然后编写另一个较小的应用程序,该应用程序基于给定的参数打开其中一个其他应用程序(然后可能关闭自身,因为它将不再需要)?


这种方法对我来说似乎是最不破坏性的。你有一个明确的责任分离,保持事情简单而精练。 - AndyM

5
我已经通过创建两个单独的应用程序来完成了这个过程。
使用此名称创建WPF应用程序:MyApp.exe。并创建控制台应用程序,其名称为:MyApp.com。当您在命令行中键入应用程序名称,例如:MyAppMyApp /help(不带.exe扩展名),具有.com扩展名的控制台应用程序将优先执行。您可以使您的控制台应用程序根据参数调用MyApp.exe
这正是devenv的行为方式。在命令行中键入devenv将启动Visual Studio的IDE。如果传递像/build这样的参数,则它将保留在命令行中。

是的,这是我过去使用的老派方法。虽然谢谢你,但我希望有另一种方法。 - Matthew
我找不到如何设置“com”扩展名。如果我在项目设置中更改“程序集名称”,它会生成“MyApp.com.exe”。 - itsho
你怎么获得MyApp.com? - FlyingMaverick

4

注意:我没有测试过,但我相信这应该可以实现...

你可以这样做:

将你的应用程序制作成Windows窗体应用程序。如果收到控制台请求,则不显示主窗体。相反,使用平台调用来调用Windows API中的Console Functions并即时分配一个控制台。

(或者,在控制台应用程序中使用API来隐藏控制台,但在这种情况下,你可能会看到控制台“闪烁”...)


2
我会创建一个解决方案,使用Windows Form应用程序,因为有两个函数可以调用,可以钩入当前的控制台。这样你就可以像处理控制台程序一样处理该程序,或者默认情况下可以启动GUI。
AttachConsole函数不会创建新的控制台。有关AttachConsole的更多信息,请查看PInvoke:AttachConsole
以下是如何使用它的示例程序。
using System.Runtime.InteropServices;

namespace Test
{
    /// <summary>
    /// This function will attach to the console given a specific ProcessID for that Console, or
    /// the program will attach to the console it was launched if -1 is passed in.
    /// </summary>
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool AttachConsole(int dwProcessId);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool FreeConsole();


    [STAThread]
    public static void Main() 
    {   
        Application.ApplicationExit +=new EventHandler(Application_ApplicationExit);
        string[] commandLineArgs = System.Environment.GetCommandLineArgs();

        if(commandLineArgs[0] == "-cmd")
        {
            //attaches the program to the running console to map the output
            AttachConsole(-1);
        }
        else
        {
            //Open new form and do UI stuff
            Form f = new Form();
            f.ShowDialog();
        }

    }

    /// <summary>
    /// Handles the cleaning up of resources after the application has been closed
    /// </summary>
    /// <param name="sender"></param>
    public static void Application_ApplicationExit(object sender, System.EventArgs e)
    {
        FreeConsole();
    }
}

2
据我所知,exe文件中有一个标志来告诉它是以控制台还是窗口应用程序运行。您可以使用Visual Studio附带的工具切换该标志,但无法在运行时执行此操作。
如果将exe编译为控制台,则如果未从控制台启动它,则始终会打开新的控制台。 如果exe是应用程序,则无法输出到控制台。您可以生成一个单独的控制台,但它不会像控制台应用程序一样运行。
过去,我们使用了两个单独的exe文件。控制台文件只是表单文件的轻量级封装(您可以像引用dll一样引用exe文件,并使用[assembly: InternalsVisibleTo("cs_friend_assemblies_2")]属性信任控制台文件,这样您就不必暴露更多信息)。

1

在调用AttachConsole()AllocConsole()后,记得要做的重要事情是使其在所有情况下都能正常工作:

if (AttachConsole(ATTACH_PARENT_PROCESS))
  {
    System.IO.StreamWriter sw =
      new System.IO.StreamWriter(System.Console.OpenStandardOutput());
    sw.AutoFlush = true;
    System.Console.SetOut(sw);
    System.Console.SetError(sw);
  }

我发现使用或不使用VS托管进程都可以工作。在调用AttachConsoleAllocConsole之前,使用System.Console.WriteLineSystem.Console.Out.WriteLine发送输出。我在下面包含了我的方法:
public static bool DoConsoleSetep(bool ClearLineIfParentConsole)
{
  if (GetConsoleWindow() != System.IntPtr.Zero)
  {
    return true;
  }
  if (AttachConsole(ATTACH_PARENT_PROCESS))
  {
    System.IO.StreamWriter sw = new System.IO.StreamWriter(System.Console.OpenStandardOutput());
    sw.AutoFlush = true;
    System.Console.SetOut(sw);
    System.Console.SetError(sw);
    ConsoleSetupWasParentConsole = true;
    if (ClearLineIfParentConsole)
    {
      // Clear command prompt since windows thinks we are a windowing app
      System.Console.CursorLeft = 0;
      char[] bl = System.Linq.Enumerable.ToArray<char>(System.Linq.Enumerable.Repeat<char>(' ', System.Console.WindowWidth - 1));
      System.Console.Write(bl);
      System.Console.CursorLeft = 0;
    }
    return true;
  }
  int Error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
  if (Error == ERROR_ACCESS_DENIED)
  {
    if (log.IsDebugEnabled) log.Debug("AttachConsole(ATTACH_PARENT_PROCESS) returned ERROR_ACCESS_DENIED");
    return true;
  }
  if (Error == ERROR_INVALID_HANDLE)
  {
    if (AllocConsole())
    {
      System.IO.StreamWriter sw = new System.IO.StreamWriter(System.Console.OpenStandardOutput());
      sw.AutoFlush = true;
      System.Console.SetOut(sw);
      System.Console.SetError(sw);
      return true;
    }
  }
  return false;
}

我在完成输出后也调用了这个命令,以防需要重新显示命令提示符。

public static void SendConsoleInputCR(bool UseConsoleSetupWasParentConsole)
{
  if (UseConsoleSetupWasParentConsole && !ConsoleSetupWasParentConsole)
  {
    return;
  }
  long LongNegOne = -1;
  System.IntPtr NegOne = new System.IntPtr(LongNegOne);
  System.IntPtr StdIn = GetStdHandle(STD_INPUT_HANDLE);
  if (StdIn == NegOne)
  {
    return;
  }
  INPUT_RECORD[] ira = new INPUT_RECORD[2];
  ira[0].EventType = KEY_EVENT;
  ira[0].KeyEvent.bKeyDown = true;
  ira[0].KeyEvent.wRepeatCount = 1;
  ira[0].KeyEvent.wVirtualKeyCode = 0;
  ira[0].KeyEvent.wVirtualScanCode = 0;
  ira[0].KeyEvent.UnicodeChar = '\r';
  ira[0].KeyEvent.dwControlKeyState = 0;
  ira[1].EventType = KEY_EVENT;
  ira[1].KeyEvent.bKeyDown = false;
  ira[1].KeyEvent.wRepeatCount = 1;
  ira[1].KeyEvent.wVirtualKeyCode = 0;
  ira[1].KeyEvent.wVirtualScanCode = 0;
  ira[1].KeyEvent.UnicodeChar = '\r';
  ira[1].KeyEvent.dwControlKeyState = 0;
  uint recs = 2;
  uint zero = 0;
  WriteConsoleInput(StdIn, ira, recs, out zero);
}

希望这有所帮助…

1

一种实现方式是编写一个 Windows 应用程序,如果命令行参数表明不需要显示窗口,则不显示窗口。

您可以始终获取命令行参数并在显示第一个窗口之前检查它们。


但是如果没有显示窗口,如何打开控制台窗口? - Lucas

0

第一项很容易。

我认为第二项做不到。

文档中说:

在 Windows 应用程序中,调用 Write 和 WriteLine 等方法没有效果。

System.Console 类在控制台和 GUI 应用程序中初始化方式不同。您可以通过在每种应用程序类型的调试器中查看 Console 类来验证此情况。不确定是否有任何重新初始化它的方法。

演示: 创建一个新的 Windows Forms 应用程序,然后将 Main 方法替换为此代码:

    static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
        else
        {
            Console.WriteLine("Console!\r\n");
        }
    }

这个想法是任何命令行参数都会在控制台打印并退出。当您不使用任何参数运行它时,您会得到窗口。但是当您用命令行参数运行它时,什么也不会发生。

然后选择项目属性,将项目类型更改为“控制台应用程序”,然后重新编译。现在,当您带有参数运行它时,您会得到您想要的“控制台!”。当您从命令行中不带参数运行它时,您将得到窗口。但是命令提示符直到您退出程序后才会返回。如果您从资源管理器运行程序,则会打开一个命令窗口,然后您会看到一个窗口。


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