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

Posted

技术标签:

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

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

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

例如,

myapp.exe /help
将输出到您使用的控制台上的标准输出,但
myapp.exe
本身会启动我的 Winforms 或 WPF 用户界面。 到目前为止,我所知道的最佳答案涉及拥有两个单独的 exe 并使用 IPC,但这感觉真的很老套。

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

【问题讨论】:

【参考方案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("Argument0 = 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();
    

【讨论】:

以上是关于如何创建一个 C# 应用程序来决定自己是显示为控制台还是窗口应用程序?的主要内容,如果未能解决你的问题,请参考以下文章

C#控制台应用程序中文显示问号 也不能复制粘贴

C#如何让panel显示滚动条

C#如何通过进程名称获取指定窗口的句柄,通过该句柄获取该窗口的标题?

如何在 Windows phone silverlight 应用程序上从 C# 填充视频

用C#写了一个控制台程序,如何让它作为服务来运行

vue中的slot用法