【问题标题】:Can one executable be both a console and GUI application?一个可执行文件可以同时是控制台和 GUI 应用程序吗?
【发布时间】:2009-01-29 21:32:07
【问题描述】:

我想制作一个C# 程序,它可以作为 CLI 或 GUI 应用程序运行,具体取决于传递给它的标志。这个可以吗?

我找到了这些相关的问题,但它们并不完全涵盖我的情况:

【问题讨论】:

标签: c# user-interface command-line-interface


【解决方案1】:

Jdigital's answer 指向Raymond Chen's blog,这解释了为什么您不能拥有一个既是控制台程序又是非控制台程序的应用程序* 程序:操作系统需要在程序开始运行之前知道 使用哪个子系统。一旦程序开始运行,再返回请求其他模式已经太晚了。

Cade's answer 指向an article about running a .Net WinForms application with a console。它使用在程序开始运行后调用AttachConsole 的技术。这具有允许程序写回启动程序的命令提示符的控制台窗口的效果。但是那篇文章中的 cmets 指出了我认为是致命的缺陷:子进程并没有真正控制控制台。控制台继续代表父进程接受输入,而父进程进程不知道它应该等待子进程完成运行,然后才能使用控制台进行其他操作。

陈的文章指向an article by Junfeng Zhang that explains a couple of other techniques

第一个是 devenv 使用的。它实际上是通过两个程序来工作的。一个是 devenv.exe,它是主要的 GUI 程序,另一个是 devenv.com,它处理控制台模式任务,但如果它用于非类似控制台的方式,它将其任务转发到 devenv.exe 并退出。该技术依赖于 Win32 规则,即当您键入不带文件扩展名的命令时,com 文件会在 exe 文件之前被选择。

Windows Script Host 有一个更简单的变体。它提供了两个完全独立的二进制文件,wscript.execscript.exe。同样,Java 为控制台程序提供 java.exe,为非控制台程序提供 javaw.exe

君峰的第二个技巧是ildasm使用的。他引用了 ildasm 的作者在两种模式下运行时所经历的过程。最终,它的作用如下:

  1. 该程序被标记为控制台模式二进制文件,因此它始终以控制台开始。这允许输入和输出重定向正常工作。
  2. 如果程序没有控制台模式命令行参数,它会重新启动自己。

仅仅调用FreeConsole 使第一个实例不再是控制台程序是不够的。这是因为启动程序 cmd.exe 的进程“知道”它启动了一个控制台模式程序并正在等待程序停止运行。调用FreeConsole 会使ildasm 停止使用控制台,但不会使父进程开始 使用控制台。

所以第一个实例会自行重新启动(我想是带有一个额外的命令行参数)。当您调用CreateProcess 时,有两个不同的标志可供尝试,DETACHED_PROCESS and CREATE_NEW_CONSOLE,其中任何一个都将确保第二个实例不会附加到父控制台。之后,第一个实例可以终止并允许命令提示符继续处理命令。

这种技术的副作用是当您从 GUI 界面启动程序时,仍然会有一个控制台。它会在屏幕上短暂闪烁,然后消失。

君峰的文章中关于使用 editbin 更改程序的控制台模式标志的部分是红鲱鱼,我认为。您的编译器或开发环境应该提供一个设置或选项来控制它创建哪种二进制文件。之后应该不需要修改任何东西。

因此,底线是您可以拥有两个二进制文件,也可以让控制台窗口瞬间闪烁。一旦您决定了哪个是较小的邪恶,您就可以选择实现。

* 我说 non-console 而不是 GUI 因为否则这是错误的二分法。程序没有控制台并不意味着它有 GUI。服务应用程序就是一个很好的例子。此外,一个程序可以有一个控制台个窗口。

【讨论】:

  • 我知道这是一个旧答案,但关于 editbin 的红鲱鱼点,我相信这个技巧的目的是让 CRT 将 WinMain 函数与适当的参数链接(所以使用/SUBSYSTEM:WINDOWS 编译,然后事后更改模式,以便加载程序启动控制台主机。如需更多反馈,我在 CreateProcess 中使用 CREATE_NO_WINDOW 尝试了此操作,并将 GetConsoleWindow() == NULL 作为我的检查是否重新启动。这并不能解决控制台闪烁问题,但确实意味着没有特殊的 cmd 参数。
  • 这是一个很好的答案,但为了完整起见,可能值得说明控制台和“非控制台”程序之间的主要区别是什么(这里的误解似乎导致下面的许多错误答案) .也就是说:从控制台启动的控制台应用程序在完成之前不会将控制权返回给父控制台,而 GUI 应用程序将分叉并立即返回。如果不确定,您可以使用 DUMPBIN /headers 并查找 SUBSYSTEM 行以查看您的确切风味。
  • 这是一个过时的最佳答案。至少从 C/C++ 的角度来看。请参阅下面 dantill 的 Win32 解决方案,可能有人可以将其改编为 C#。
  • 我不认为这个答案已经过时。该方法效果很好,答案的评分不言而喻。 Dantill 的方法将标准输入与控制台应用程序断开连接。我在下面提供了肯尼迪“瞬时闪烁”方法的 C 版本作为单独的答案(是的,我知道,OP 发布了关于 C# 的内容)。我已经用过好几次了,很满意。
  • 我认为你误解了这个问题,@Antoniossss。目标是可以作为任何一种程序运行的单个二进制文件,在它的选择,而不是一个同时运行的二进制文件。后者很容易。前者不是,只能通过各种“伪装”来实现。
【解决方案2】:

查看 Raymond 关于此主题的博客:

https://devblogs.microsoft.com/oldnewthing/20090101-00/?p=19643

他的第一句话:“你不能,但你可以试着伪造它。”

【讨论】:

  • .Net 实际上很容易“伪造”,但这个答案在技术上是正确的。
【解决方案3】:

http://www.csharp411.com/console-output-from-winforms-application/

只需检查 WinForms Application. 之前的命令行参数即可。

我应该补充一点,在 .NET 中,简单地在同一个解决方案中创建一个控制台和 GUI 项目是非常容易的,这些项目共享除 main 之外的所有程序集。在这种情况下,您可以让命令行版本简单地启动 GUI 版本,如果它是在没有参数的情况下启动的。你会得到一个闪烁的控制台。

【讨论】:

  • 命令行参数的存在并不是一个确定的火灾指示。许多 Windows 应用程序都可以使用命令行参数
  • 我的意思是,如果没有,请启动 GUI 版本。如果你想启动带有参数的 GUI 版本,想必你可以有一个参数。
【解决方案4】:

有一种简单的方法可以做你想做的事。在编写应该同时具有 CLI 和 GUI 的应用程序时,我总是使用它。您必须将“OutputType”设置为“ConsoleApplication”才能使用。

class Program {
  [DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow")]
  private static extern IntPtr _GetConsoleWindow();

  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main(string[] args) {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    /*
     * This works as following:
     * First we look for command line parameters and if there are any of them present, we run the CLI version.
     * If there are no parameters, we try to find out if we are run inside a console and if so, we spawn a new copy of ourselves without a console.
     * If there is no console at all, we show the GUI.
     * We make an exception if we find out, that we're running inside visual studio to allow for easier debugging the GUI part.
     * This way we're both a CLI and a GUI.
     */
    if (args != null && args.Length > 0) {

      // execute CLI - at least this is what I call, passing the given args.
      // Change this call to match your program.
      CLI.ParseCommandLineArguments(args);

    } else {
      var consoleHandle = _GetConsoleWindow();

      // run GUI
      if (consoleHandle == IntPtr.Zero || AppDomain.CurrentDomain.FriendlyName.Contains(".vshost"))

        // we either have no console window or we're started from within visual studio
        // This is the form I usually run. Change it to match your code.
        Application.Run(new MainForm());
      else {

        // we found a console attached to us, so restart ourselves without one
        Process.Start(new ProcessStartInfo(Assembly.GetEntryAssembly().Location) {
          CreateNoWindow = true,
          UseShellExecute = false
        });
      }
    }
  }

【讨论】:

  • 我喜欢这个,它在我的 Windows 7 开发机器上运行良好。但是我有一台(虚拟)Windows XP 机器,似乎重新启动的进程总是得到一个控制台,因此无休止地消失循环重新启动自身。有什么想法吗?
  • 对此要非常小心,在 Windows XP 上,这确实会导致无限的重生循环,很难杀死。
【解决方案5】:

我认为首选技术是 Rob 所说的 devenv 技术,该技术使用两个可执行文件:启动器“.com”和原始“.exe”。如果您有样板代码可以使用,这并不难使用(请参阅下面的链接)。

该技术使用技巧让“.com”成为 stdin/stdout/stderr 的代理并启动同名的 .exe 文件。这给出了允许程序在从控制台调用时以命令行模式执行的行为(可能仅在检测到某些命令行参数时),同时仍然能够作为没有控制台的 GUI 应用程序启动。

我主持了一个名为 dualsubsystem on Google Code 的项目,该项目更新了该技术的旧 codeguru 解决方案,并提供了源代码和工作示例二进制文件。

【讨论】:

    【解决方案6】:

    我认为这是解决该问题的简单 .NET C# 解决方案。只是为了重申问题,当您从带有开关的命令行运行应用程序的控制台“版本”时,即使您有Environment.Exit(0) 在代码末尾。要解决此问题,请在调用 Environment.Exit(0) 之前调用:

    SendKeys.SendWait("{ENTER}");
    

    然后控制台获得返回命令提示符所需的最终 Enter 键,该过程结束。注意:不要拨打SendKeys.Send(),否则应用会崩溃。

    正如许多帖子中提到的那样,仍然需要调用AttachConsole(),但是在启动应用程序的 WinForm 版本时,我没有看到命令窗口闪烁。

    这是我创建的示例应用程序中的完整代码(没有 WinForms 代码):

    using System;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    
    namespace ConsoleWriter
    {
        static class Program
        {
            [DllImport("kernel32.dll")]
            private static extern bool AttachConsole(int dwProcessId);
            private const int ATTACH_PARENT_PROCESS = -1;
    
            [STAThread]
            static void Main(string[] args)
            {
                if(args.Length > 0 && args[0].ToUpperInvariant() == "/NOGUI")
                {
                    AttachConsole(ATTACH_PARENT_PROCESS);
                    Console.WriteLine(Environment.NewLine + "This line prints on console.");
    
                    Console.WriteLine("Exiting...");
                    SendKeys.SendWait("{ENTER}");
                    Environment.Exit(0);
                }
                else
                {
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    Application.Run(new Form1());
                }
            }
        }
    }
    

    希望它可以帮助某人在这个问题上也花费数天时间。感谢您的提示去@dantill。

    【讨论】:

    • 我试过这个,问题是使用Console.WriteLine 编写的任何内容都不会推进(父)控制台的文本光标。因此,当您的应用退出时,光标位置在错误的位置,您必须按几次 Enter 才能使其返回“干净”提示。
    • @TahirHassan 您可以按照此处所述自动进行提示捕获和清理,但这仍然不是一个完美的解决方案:stackoverflow.com/questions/1305257/…
    【解决方案7】:
    /*
    ** dual.c    Runs as both CONSOLE and GUI app in Windows.
    **
    ** This solution is based on the "Momentary Flicker" solution that Robert Kennedy
    ** discusses in the highest-rated answer (as of Jan 2013), i.e. the one drawback
    ** is that the console window will briefly flash up when run as a GUI.  If you
    ** want to avoid this, you can create a shortcut to the executable and tell the
    ** short cut to run minimized.  That will minimize the console window (which then
    ** immediately quits), but not the GUI window.  If you want the GUI window to
    ** also run minimized, you have to also put -minimized on the command line.
    **
    ** Tested under MinGW:  gcc -o dual.exe dual.c -lgdi32
    **
    */
    #include <windows.h>
    #include <stdio.h>
    
    static int my_win_main(HINSTANCE hInstance,int argc,char *argv[],int iCmdShow);
    static LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,LPARAM lParam);
    static int win_started_from_console(void);
    static BOOL CALLBACK find_win_by_procid(HWND hwnd,LPARAM lp);
    
    int main(int argc,char *argv[])
    
        {
        HINSTANCE hinst;
        int i,gui,relaunch,minimized,started_from_console;
    
        /*
        ** If not run from command-line, or if run with "-gui" option, then GUI mode
        ** Otherwise, CONSOLE app.
        */
        started_from_console = win_started_from_console();
        gui = !started_from_console;
        relaunch=0;
        minimized=0;
        /*
        ** Check command options for forced GUI and/or re-launch
        */
        for (i=1;i<argc;i++)
            {
            if (!strcmp(argv[i],"-minimized"))
                minimized=1;
            if (!strcmp(argv[i],"-gui"))
                gui=1;
            if (!strcmp(argv[i],"-gui-"))
                gui=0;
            if (!strcmp(argv[i],"-relaunch"))
                relaunch=1;
            }
        if (!gui && !relaunch)
            {
            /* RUN AS CONSOLE APP */
            printf("Console app only.\n");
            printf("Usage:  dual [-gui[-]] [-minimized].\n\n");
            if (!started_from_console)
                {
                char buf[16];
                printf("Press <Enter> to exit.\n");
                fgets(buf,15,stdin);
                }
            return(0);
            }
    
        /* GUI mode */
        /*
        ** If started from CONSOLE, but want to run in GUI mode, need to re-launch
        ** application to completely separate it from the console that started it.
        **
        ** Technically, we don't have to re-launch if we are not started from
        ** a console to begin with, but by re-launching we can avoid the flicker of
        ** the console window when we start if we start from a shortcut which tells
        ** us to run minimized.
        **
        ** If the user puts "-minimized" on the command-line, then there's
        ** no point to re-launching when double-clicked.
        */
        if (!relaunch && (started_from_console || !minimized))
            {
            char exename[256];
            char buf[512];
            STARTUPINFO si;
            PROCESS_INFORMATION pi;
    
            GetStartupInfo(&si);
            GetModuleFileNameA(NULL,exename,255);
            sprintf(buf,"\"%s\" -relaunch",exename);
            for (i=1;i<argc;i++)
                {
                if (strlen(argv[i])+3+strlen(buf) > 511)
                    break;
                sprintf(&buf[strlen(buf)]," \"%s\"",argv[i]);
                }
            memset(&pi,0,sizeof(PROCESS_INFORMATION));
            memset(&si,0,sizeof(STARTUPINFO));
            si.cb = sizeof(STARTUPINFO);
            si.dwX = 0; /* Ignored unless si.dwFlags |= STARTF_USEPOSITION */
            si.dwY = 0;
            si.dwXSize = 0; /* Ignored unless si.dwFlags |= STARTF_USESIZE */
            si.dwYSize = 0;
            si.dwFlags = STARTF_USESHOWWINDOW;
            si.wShowWindow = SW_SHOWNORMAL;
            /*
            ** Note that launching ourselves from a console will NOT create new console.
            */
            CreateProcess(exename,buf,0,0,1,DETACHED_PROCESS,0,NULL,&si,&pi);
            return(10); /* Re-launched return code */
            }
        /*
        ** GUI code starts here
        */
        hinst=GetModuleHandle(NULL);
        /* Free the console that we started with */
        FreeConsole();
        /* GUI call with functionality of WinMain */
        return(my_win_main(hinst,argc,argv,minimized ? SW_MINIMIZE : SW_SHOWNORMAL));
        }
    
    
    static int my_win_main(HINSTANCE hInstance,int argc,char *argv[],int iCmdShow)
    
        {
        HWND        hwnd;
        MSG         msg;
        WNDCLASSEX  wndclass;
        static char *wintitle="GUI Window";
    
        wndclass.cbSize        = sizeof (wndclass) ;
        wndclass.style         = CS_HREDRAW | CS_VREDRAW;
        wndclass.lpfnWndProc   = WndProc;
        wndclass.cbClsExtra    = 0 ;
        wndclass.cbWndExtra    = 0 ;
        wndclass.hInstance     = hInstance;
        wndclass.hIcon         = NULL;
        wndclass.hCursor       = NULL;
        wndclass.hbrBackground = NULL;
        wndclass.lpszMenuName  = NULL;
        wndclass.lpszClassName = wintitle;
        wndclass.hIconSm       = NULL;
        RegisterClassEx (&wndclass) ;
    
        hwnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,wintitle,0,
                              WS_VISIBLE|WS_OVERLAPPEDWINDOW,
                              100,100,400,200,NULL,NULL,hInstance,NULL);
        SetWindowText(hwnd,wintitle);
        ShowWindow(hwnd,iCmdShow);
        while (GetMessage(&msg,NULL,0,0))
            {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            }
        return(msg.wParam);
        }
    
    
    static LRESULT CALLBACK WndProc (HWND hwnd,UINT iMsg,WPARAM wParam,LPARAM lParam)
    
        {
        if (iMsg==WM_DESTROY)
            {
            PostQuitMessage(0);
            return(0);
            }
        return(DefWindowProc(hwnd,iMsg,wParam,lParam));
        }
    
    
    static int fwbp_pid;
    static int fwbp_count;
    static int win_started_from_console(void)
    
        {
        fwbp_pid=GetCurrentProcessId();
        if (fwbp_pid==0)
            return(0);
        fwbp_count=0;
        EnumWindows((WNDENUMPROC)find_win_by_procid,0L);
        return(fwbp_count==0);
        }
    
    
    static BOOL CALLBACK find_win_by_procid(HWND hwnd,LPARAM lp)
    
        {
        int pid;
    
        GetWindowThreadProcessId(hwnd,(LPDWORD)&pid);
        if (pid==fwbp_pid)
            fwbp_count++;
        return(TRUE);
        }
    

    【讨论】:

      【解决方案8】:

      我已经编写了一种替代方法来避免控制台闪烁。请参阅How to create a Windows program that works both as a GUI and console application

      【讨论】:

      • 我持怀疑态度,但它完美无缺。就像真的,真的完美无瑕。极好的工作!我见过的第一个真正解决问题的方法。 (这是 C/C++ 代码。不是 C# 代码。)
      • 我同意 B. Nadolson 的观点。这有效(对于 C++),无需重新启动进程,也无需多个 EXE。
      • 此方法的缺点:(1)它必须在完成后向控制台发送额外的击键,(2)它不能将控制台输出重定向到文件,以及(3)它显然有未使用附加的标准输入进行测试(我猜也无法从文件中重定向)。对我来说,为了避免暂时弹出控制台窗口而进行的交易太多了。重新启动方法至少提供了真正的双控制台/GUI。我已经向数以万计的用户分发了这样一个应用程序,但没有收到任何关于控制台窗口瞬间闪烁的投诉或评论。
      【解决方案9】:

      在静态构造函数中运行 AllocConsole() 对我有用

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-22
        • 2015-11-03
        • 1970-01-01
        • 1970-01-01
        • 2010-12-10
        • 2015-11-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多