一个可执行文件既可以是控制台应用程序又可以是GUI应用程序吗?

97

请查看这个问题:http://stackoverflow.com/questions/199182/c-hiding-form-when-running-form-program-from-the-command-line - benPearce
1
只是为了记录:这实际上与CLR无关,而是与操作系统有关。例如,在Linux上使用Mono创建这样的应用程序没有问题(实际上,每个应用程序都是控制台,但也可以在窗口中执行操作),就像使用Java或任何其他*nix程序一样。通常模式是在控制台上记录日志,同时为用户使用GUI。 - konrad.kruczynski
9个回答

115

Jdigital的回答指向Raymond Chen的博客,解释了为什么一个应用程序不能同时作为控制台程序和非控制台* 程序:操作系统需要在程序开始运行之前就知道使用哪个子系统。一旦程序开始运行,就太晚了,无法返回并请求其他模式。

Cade的回答指向一篇关于在 .Net WinForms 应用程序中运行控制台的文章。它使用在程序开始运行后调用AttachConsole的技术。这会使程序能够写回到启动该程序的命令提示符的控制台窗口。但是,该文章中的评论指出了我认为是致命缺陷:子进程并不能真正控制控制台。控制台继续代表父进程接受输入,而父进程不知道应该在使用控制台进行其他操作之前等待子进程完成运行。

Chen的文章指向一篇由Junfeng Zhang撰写的解释其他几种技术的文章

第一个技术是devenv使用的。它实际上有两个程序。其中一个是主GUI程序devenv.exe,另一个是处理控制台模式任务的devenv.com,但如果以非控制台方式使用它,则将其任务转发给devenv.exe并退出。该技术依赖于Win32规则,即在输入没有文件扩展名的命令时,com文件优先于exe文件被选择。
这里有一个更简单的变体由Windows脚本主机执行。它提供了两个完全独立的二进制文件,wscript.execscript.exe。同样,Java为控制台程序提供java.exe,为非控制台程序提供javaw.exe
Junfeng的第二种技术是ildasm使用的。他引用了ildasm作者在使其以两种模式运行时所经历的过程。最终,以下是它所做的:
1. 将程序标记为控制台模式二进制文件,因此它始终带有控制台。这允许输入和输出重定向正常工作。 2. 如果程序没有控制台模式命令行参数,则重新启动它本身。
仅仅调用FreeConsole不能使第一个实例停止成为控制台程序。这是因为启动程序的过程cmd.exe,“知道”它启动了控制台模式程序,并等待程序停止运行。调用FreeConsole会使ildasm停止使用控制台,但不会使父进程开始使用控制台。因此,第一个实例会重新启动自己(我想会带有额外的命令行参数)。调用CreateProcess时,有两个不同的标志可以尝试:DETACHED_PROCESSCREATE_NEW_CONSOLE,其中任何一个都将确保第二个实例不会附加到父控制台。之后,第一个实例可以终止并允许命令提示符继续处理命令。

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

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

因此,您可以拥有两个二进制文件,或者您可以有一个短暂的控制台窗口闪烁。一旦您决定哪个是较小的邪恶,就可以选择实现。

*我说非控制台而不是 GUI ,因为否则这是一个虚假的二分法。仅因为程序没有控制台并不意味着它具有GUI。服务应用程序就是一个典型例子。此外,程序可以拥有控制台和窗口。


1
我知道这是一个旧答案,但关于editbin的红鲱鱼点,我相信那个技巧的目的是让CRT链接一个带有适当参数的WinMain函数(因此使用/SUBSYSTEM:WINDOWS编译),然后在事后更改模式,以便加载器启动控制台主机。为了获得更多反馈,我尝试了在CreateProcess中使用CREATE_NO_WINDOWGetConsoleWindow() == NULL作为我的检查是否重新启动的标志。这并不能解决控制台闪烁的问题,但它确实意味着不需要特殊的cmd参数。 - user257111
这是一个很好的答案,但为了完整起见,可能值得说明一下控制台和“非控制台”程序之间的主要区别(误解似乎导致了下面许多错误的答案)。也就是说:从控制台启动的控制台应用程序在完成之前不会将控制权返回给父控制台,而GUI应用程序将分叉并立即返回。 当不确定时,您可以使用DUMPBIN / headers并查找SUBSYSTEM行,以查看您拥有的确切版本。 - piers7
这是一个过时的最佳答案。至少从C/C++的角度来看是这样的。请参见下面dantill的Win32解决方案,这可能可以被某人改编为C#。 - B. Nadolson
1
我不认为这个答案已经过时。该方法运行良好,且答案的评分足以证明其有效性。Dantill的方法将stdin从控制台应用程序中断开连接。我在下面单独回答了肯尼迪的“短暂闪烁”方法的C版本(是的,我知道,原始帖子是关于C#的)。我已经多次使用它并非常满意。 - willus
1
我认为你误解了问题,@Antoniossss。目标是一个单一的二进制文件,可以根据需要表现出任何一种程序的行为,而不是同时表现出两种程序的行为。后者很容易实现。前者则不然,只能通过各种程度的“假装”来实现。 - Rob Kennedy
显示剩余6条评论

11

.Net 实际上很容易“伪造”,但这个答案在技术上是正确的。 - Joel Coehoorn

6

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

在使用WinForms的Application.命令之前,请检查命令行参数。

我应该补充说明,在.NET中,将控制台和GUI项目放在同一个解决方案中并共享所有程序集(除了主要程序集)非常容易。 在这种情况下,如果不使用任何参数启动命令行版本,则可以使其简单地启动GUI版本。你会看到一个闪烁的控制台。


命令行参数的存在很难确定一个应用程序是否是Windows应用程序。许多Windows应用程序都可以使用命令行参数。 - Neil N
3
我的意思是如果没有命令行参数,就启动GUI版本。 如果您希望使用参数启动GUI版本,可能可以为此设置一个参数。 - Cade Roux

5

有一个简单的方法可以实现你想要的功能。当我编写既有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
        });
      }
    }
  }

1
我喜欢这个程序,在我的Windows 7开发机上运行良好。然而,我有一个(虚拟的)Windows XP机器,重新启动后进程似乎总是得到一个控制台,然后在无限循环中重启自己。有什么想法吗? - Simon Hewitt
1
一定要非常小心,因为在Windows XP上,这确实会导致一个无限重生循环,非常难以终止。 - user

3
我认为首选技术是Rob所称的“devenv”技术,使用两个可执行文件:一个启动器“.com”和原始的“.exe”。如果您有样板代码可以使用,那么使用这种技术并不那么棘手(请参见下面的链接)。
该技术使用技巧,使得“.com”成为标准输入/输出/错误的代理,并启动同名的.exe文件。这使得程序在从控制台调用时能够以命令行模式运行(可能仅当检测到某些命令行参数时),同时仍能够在没有控制台的情况下作为GUI应用程序启动。
我托管了一个名为dualsubsystem on Google Code的项目,更新了这种技术的旧codeguru解决方案,并提供源代码和工作示例二进制文件。

3

以下是我认为的解决问题的简单.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 写入的任何内容都不会推进(父)控制台的文本光标。因此,当您的应用程序退出时,光标位置就会出现错误,并且您必须按几次回车键才能将其恢复到“干净”的提示符状态。 - Tahir Hassan
@TahirHassan 您可以按照此处描述的方式自动捕获和清理提示,但这仍然不是完美的解决方案:https://dev59.com/n3M_5IYBdhLWcg3wmkeu#59340459 - rkagerer

2

1
我一开始持怀疑态度,但它完美无缺地运行。真的,真的非常流畅。做得非常好!这是我见过的第一个真正解决问题的方案。 - B. Nadolson
我同意B. Nadolson的观点。这个方法(适用于C++)可以在不重新启动进程和不使用多个EXE的情况下实现。 - GravityWell
2
这种方法的缺点是:(1) 它必须在完成时向控制台发送额外的按键,(2) 它无法将控制台输出重定向到文件,(3) 显然它还没有经过附加 stdin 的测试(我猜也无法从文件中重定向)。对我来说,这样做需要太多的交易,只是为了避免短暂地闪现控制台窗口。重新启动方法至少提供了真正的双控制台/GUI。我已经将这样的应用程序分发给数万用户,并没有收到任何关于短暂闪现控制台窗口的投诉或评论。 - willus

2
/*
** 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);
    }

0

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


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