如何创建一个C#应用程序来决定自己是显示为控制台还是窗口应用程序?

时间:2009-04-30 17:00:03

标签: c# .net wpf winforms

有没有办法启动具有以下功能的C#应用​​程序?

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

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

    到目前为止,我所知道的最佳答案涉及两个独立的exe和使用IPC,但这感觉非常hacky。


    我可以选择哪些选项并进行权衡以获得上述示例中描述的行为?我也愿意接受特定于Winform或WPF的想法。

12 个答案:

答案 0 :(得分:57)

使应用成为常规Windows应用,并在需要时动态创建控制台。

this link的更多详细信息(以下代码)

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 :(得分:14)

我基本上按照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
            if (!AttachConsole(-1))
                AllocConsole();

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

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

            return;
        }

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

    }

答案 2 :(得分:7)

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

答案 3 :(得分:5)

我通过创建两个单独的应用程序来完成此操作。

使用以下名称创建WPF应用:MyApp.exe。并使用以下名称创建控制台应用程序:MyApp.com。当您在命令行中输入应用名称时,例如此MyAppMyApp /help(不带.exe扩展名),具有.com扩展名的控制台应用优先。您可以让控制台应用程序根据参数调用MyApp.exe

这正是devenv的行为方式。在命令行键入devenv将启动Visual Studio的IDE。如果传递/build之类的参数,它将保留在命令行中。

答案 4 :(得分:4)

注意:我没有测试过这个,但我相信它会起作用......

你可以这样做:

使您的应用成为Windows窗体应用程序。如果您收到控制台请求,请不要显示主表单。相反,使用平台调用来调用Windows API中的Console Functions并动态分配控制台。

(或者,使用API​​在控制台应用程序中隐藏控制台,但您可能会看到控制台“闪烁”,因为它是在这种情况下创建的......)

答案 5 :(得分:2)

据我所知,exe中有一个标志,告诉它是以控制台还是窗口应用程序运行。您可以使用Visual Studio附带的工具轻弹该标志,但是您无法在运行时执行此操作。

如果exe编译为控制台,那么如果它没有从一个控制台启动,它将始终打开一个新的控制台。 如果exe是一个应用程序,那么它无法输出到控制台。您可以生成一个单独的控制台 - 但它不会像控制台应用程序那样。

我过去我们使用了2个单独的exe。控制台一个是表单上的一个瘦包装器(你可以像引用一个dll那样引用一个exe,你可以使用[assembly:InternalsVisibleTo(“cs_friend_assemblies_2”)]属性来信任控制台一个,所以你不要必须暴露超出你需要的。)

答案 6 :(得分:2)

我会创建一个Windows窗体应用程序的解决方案,因为您可以调用两个可以挂钩到当前控制台的函数。所以你可以像控制台程序一样对待程序。或者默认情况下,您可以启动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();
    }
}

答案 7 :(得分:1)

也许这个link会提供一些有关您希望做什么的见解。

答案 8 :(得分:1)

执行此操作的一种方法是编写一个不显示窗口的Window应用程序,如果命令行参数表明它不应该。

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

答案 9 :(得分: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主机进程的工作。在调用System.Console.WriteLineSystem.Console.out.WriteLine之前,输出与AttachConsoleAllocConsole一起发送。我在下面列出了我的方法:

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

希望这会有所帮助......

答案 10 :(得分:0)

不容易。

没有2不能做,我不认为。

文档说:

  

调用Write和WriteLine等方法对Windows应用程序没有影响。

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

演示: 创建一个新的Windows窗体应用程序,然后用以下方法替换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");
        }
    }

这个想法是任何命令行参数都将打印到控制台并退出。当你没有参数运行它时,你会得到窗口。但是当你使用命令行参数运行它时,没有任何反应。

然后选择项目属性,将项目类型更改为“Console Application”,然后重新编译。现在,当您使用参数运行它时,您将获得“控制台!”像你要的那样。当你运行它(从命令行)没有参数时,你得到了窗口。但是在退出程序之前,命令提示符不会返回。如果您从资源管理器运行程序,将打开一个命令窗口,然后您将获得一个窗口。

答案 11 :(得分:0)

我已经找到了一种方法来做到这一点,包括使用stdin,但我必须警告你它不漂亮。

从连接的控制台使用stdin的问题是shell也会从中读取。这会导致输入有时会转到您的应用程序,但有时会转到shell。

解决方案是在应用程序生命周期内阻止shell(尽管从技术上讲,您可以尝试仅在需要输入时阻止它)。我选择这样做的方法是向shell发送键击以运行powershell命令,该命令等待应用程序终止。

顺便提一下,这也解决了应用程序终止后提示无法返回的问题。

我已经简单地尝试过从PowerShell控制台开始工作。同样的原则适用,但我没有让它执行我的命令。 PowerShell可能会进行一些安全检查以防止从其他应用程序运行命令。因为我没有使用powershell,所以我没有调查它。

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

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool AttachConsole(int dwProcessId);

    private const uint STD_INPUT_HANDLE = 0xfffffff6;
    private const uint STD_OUTPUT_HANDLE = 0xfffffff5;
    private const uint STD_ERROR_HANDLE = 0xfffffff4;

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(uint nStdHandle);
    [DllImport("Kernel32.dll", SetLastError = true)]
    public static extern int SetStdHandle(uint nStdHandle, IntPtr handle);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern int GetConsoleProcessList(int[] ProcessList, int ProcessCount);

    [DllImport("user32.dll")]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    public static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    /// <summary>
    /// Attach to existing console or create new. Must be called before using System.Console.
    /// </summary>
    /// <returns>Return true if console exists or is created.</returns>
    public static bool InitConsole(bool createConsole = false, bool suspendHost = true) {

        // first try to attach to an existing console
        if (AttachConsole(-1)) {
            if (suspendHost) {
                // to suspend the host first try to find the parent
                var processes = GetConsoleProcessList();

                Process host = null;
                string blockingCommand = null;

                foreach (var proc in processes) {
                    var netproc = Process.GetProcessById(proc);
                    var processName = netproc.ProcessName;
                    Console.WriteLine(processName);
                    if (processName.Equals("cmd", StringComparison.OrdinalIgnoreCase)) {
                        host = netproc;
                        blockingCommand = $"powershell \"& wait-process -id {Process.GetCurrentProcess().Id}\"";
                    } else if (processName.Equals("powershell", StringComparison.OrdinalIgnoreCase)) {
                        host = netproc;
                        blockingCommand = $"wait-process -id {Process.GetCurrentProcess().Id}";
                    }
                }

                if (host != null) {
                    // if a parent is found send keystrokes to simulate a command
                    var cmdWindow = host.MainWindowHandle;
                    if (cmdWindow == IntPtr.Zero) Console.WriteLine("Main Window null");

                    foreach (char key in blockingCommand) {
                        SendChar(cmdWindow, key);
                        System.Threading.Thread.Sleep(1); // required for powershell
                    }

                    SendKeyDown(cmdWindow, Keys.Enter);

                    // i haven't worked out how to get powershell to accept a command, it might be that this is a security feature of powershell
                    if (host.ProcessName == "powershell") Console.WriteLine("\r\n *** PRESS ENTER ***");
                }
            }

            return true;
        } else if (createConsole) {
            return AllocConsole();
        } else {
            return false;
        }
    }

    private static void SendChar(IntPtr cmdWindow, char k) {
        const uint WM_CHAR = 0x0102;

        IntPtr result = PostMessage(cmdWindow, WM_CHAR, ((IntPtr)k), IntPtr.Zero);
    }

    private static void SendKeyDown(IntPtr cmdWindow, Keys k) {
        const uint WM_KEYDOWN = 0x100;
        const uint WM_KEYUP = 0x101;

        IntPtr result = SendMessage(cmdWindow, WM_KEYDOWN, ((IntPtr)k), IntPtr.Zero);
        System.Threading.Thread.Sleep(1);
        IntPtr result2 = SendMessage(cmdWindow, WM_KEYUP, ((IntPtr)k), IntPtr.Zero);
    }

    public static int[] GetConsoleProcessList() {
        int processCount = 16;
        int[] processList = new int[processCount];

        // supposedly calling it with null/zero should return the count but it didn't work for me at the time
        // limiting it to a fixed number if fine for now
        processCount = GetConsoleProcessList(processList, processCount);
        if (processCount <= 0 || processCount >= processList.Length) return null; // some sanity checks

        return processList.Take(processCount).ToArray();
    }