【问题标题】:How do I create a C# app that decides itself whether to show as a console or windowed app?如何创建一个 C# 应用程序来决定自己是显示为控制台还是窗口应用程序?
【发布时间】:2010-10-22 22:06:38
【问题描述】:

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

  1. 通过命令行参数判断是窗口应用还是控制台应用
  2. 当它被要求窗口化时不显示控制台,从控制台运行时也不显示 GUI 窗口。

例如,

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

到目前为止,我所知道的最佳答案涉及拥有两个单独的 exe 并使用 IPC,但这感觉真的很老套。


为了获得上例中描述的行为,我有哪些选择和权衡?我也对特定于 Winform 或 WPF 的想法持开放态度。

【问题讨论】:

    标签: c# .net wpf winforms


    【解决方案1】:

    将应用程序设为常规 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);
    
      }
    }
    

    【讨论】:

    • 这不会创建一个新的控制台窗口吗?我认为最初的问题是当用户键入 MyApp.exe /help 时输出到“您使用的控制台”。
    • 您是对的 - 要附加到现有控制台,您将使用 AttachConsole(-1)。更新了代码以反映这一点。
    • 这种方法的最大问题似乎是当您从 shell 启动时,进程会立即返回到启动控制台。在这种情况下,当您使用该外壳/控制台时,标准输出开始覆盖其他文本的其余部分。如果有办法避免这种情况也会非常方便,但我还没有找到任何东西。
    • 大多数 WinForm 应用程序都有一个选项可以在您关闭主窗体时关闭可执行文件。如果您遵循使用主窗体的约定,您可以在主窗体上执行 .ShowDialog,因此当它关闭时,整个应用程序都会关闭。我在下面的回答有一个例子。
    【解决方案2】:

    我基本上按照 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());
    
        }
    

    【讨论】:

    • 这应该是答案。 SendKeys 很重要。
    【解决方案3】:

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

    【讨论】:

    • 这种方法对我来说似乎是最简单的。你有一个明确的关注点分离,并保持精简和简单。
    【解决方案4】:

    我通过创建两个单独的应用程序来做到这一点。

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

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

    【讨论】:

    • 是的,这是我过去使用的老式方法。我希望有另一种方法。不过,谢谢。
    • 我找不到如何设置com 扩展名。如果我在项目设置中更改Assembly name,它会生成MyApp.com.exe
    • 如何获得 MyApp.com?
    【解决方案5】:

    注意:我尚未对此进行测试,但我相信它会起作用...

    你可以这样做:

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

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

    【讨论】:

      【解决方案6】:

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

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

      过去我们使用了 2 个单独的 exe。控制台一是表单一的薄包装器(您可以像引用 dll 一样引用 exe,并且您可以使用 [assembly:InternalsVisibleTo("cs_friend_assemblies_2")] 属性来信任控制台一,所以您不要不必暴露比你需要的更多)。

      【讨论】:

        【解决方案7】:

        我将创建一个 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();
            }
        }
        

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

          • 但是不显示窗口时打开控制台窗口怎么办?
          【解决方案9】:

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

          希望这会有所帮助...

          【讨论】:

            【解决方案10】:

            没有 1 很容易。

            第 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】:

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

              从附加控制台使用标准输入的问题是外壳也会从中读取。这会导致输入有时会进入您的应用,但有时会进入 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();
                  }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-04-04
                • 2011-10-09
                • 2017-08-03
                • 2017-09-09
                • 2010-10-01
                相关资源
                最近更新 更多