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


80

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

我发现了以下相关问题,但它们并不能完全满足我的情况:



1
仅作记录:它确实与操作系统有关,而不与CLR有关。例如,在Linux上使用Mono时,创建此类应用程序没有问题(实际上,每个应用程序都是控制台,但也可以在Windows上执行任何操作),就像Java或任何其他* nix程序一样。常见的模式是在为用户使用GUI时在控制台上进行登录。
konrad.kruczynski,2012年

Answers:


98

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

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

Chen的文章指向Zhang Junfeng Zhang的文章,它解释了其他两种技术

首先是devenv的用途。它实际上有两个程序。一个是主界面GUI程序devenv.exe,另一个是处理控制台模式任务的devenv.com,但是如果以非控制台方式使用它,则它将其任务转发给devenv.exe和退出。该技术依赖于Win32规则,当您键入不带文件扩展名的命令时,将在exe文件之前选择com文件。

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

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

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

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

因此,第一个实例将自行重启(我想带有一个额外的命令行参数)。当您调用时CreateProcess,有两个不同的标志可以尝试使用,分别是DETACHED_PROCESSCREATE_NEW_CONSOLE,这两个标志将确保第二个实例不会附加到父控制台。此后,第一个实例可以终止并允许命令提示符恢复处理命令。

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

我认为,Junfeng文章中有关使用editbin更改程序的控制台模式标志的部分是一个红色的鲱鱼。您的编译器或开发环境应提供一个设置或选项来控制它创建哪种二进制文件。此后无需再进行任何修改。

最重要的是,您可以具有两个二进制文件,也可以具有控制台窗口的瞬时闪烁。一旦确定哪个是较小的邪恶,就可以选择实现。

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


我知道这是一个老答案,但是在有关editbin的要点上,我相信该技巧的目的是让CRT将WinMain函数与适当的参数链接(使用进行编译/SUBSYSTEM:WINDOWS),然后事后更改模式,因此加载程序将启动控制台主机。为了获得更多反馈,我尝试CREATE_NO_WINDOW在CreateProcess中进行此操作,并GetConsoleWindow() == NULL检查是否重新启动。这不能解决控制台闪烁问题,但确实意味着没有特殊的cmd参数。

这是一个很好的答案,但是为了完整起见,可能有必要说明控制台程序和“非控制台”程序之间的主要区别是什么(这里的误解似乎导致以下许多错误的答案)。也就是说:从控制台启动的控制台应用程序在完成之前不会将控制权交还给父控制台,而GUI应用程序将派生并立即返回。不确定时,可以使用DUMPBIN / headers并查找SUBSYSTEM行,以确切地了解所具有的风味。
7

这是一个过时的最佳答案。至少从C / C ++角度来看。请参阅下面针对Win32的dantill解决方案,该解决方案可能会被某人改编为C#。
B. Nadolson

1
我认为这个答案已经过时了。该方法效果很好,答案的评分不言而喻。Dantill的方法将stdin与控制台应用程序断开连接。我在下面提供了Kennedy的“瞬间闪烁”方法的C版本作为单独的答案(是的,我知道,OP发布了有关C#的内容)。我已经使用过几次了,对此非常满意。
willus 2015年

您可以在Java中执行此操作。.)
Antoniossss19年


6

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

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

我应该补充一点,在.NET中,只需在同一解决方案中简单地创建一个控制台和GUI项目,即可共享主组件之外的所有组件,这非常容易。并且在这种情况下,如果不带参数启动命令行版本,则只需启动GUI版本即可。您将获得一个闪烁的控制台。


命令行参数的存在很难确定是否有火苗。大量Windows应用程序都可以使用命令行参数
Neil N

3
我的观点是,如果没有,请启动GUI版本。如果要使用参数启动GUI版本,则大概可以有一个参数。
卡德·鲁

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机器,似乎重新启动的进程总会得到一个控制台,因此消失在无休止的循环中,重新启动了自己。有任何想法吗?
西蒙·休伊特

1
对此要非常小心,在Windows XP上确实会导致无限重生循环,这很难消除。
用户

3

我认为首选技术是Rob所说的使用两个可执行文件的devenv技术:启动程序“ .com”和原始的“ .exe”。如果您要使用样板代码,则使用它并不是那么棘手(请参见下面的链接)。

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

在Google Code上托管了一个名为dualsubsystem的项目,该项目更新了该技术的旧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都不会推进(父)控制台的文本光标。因此,当您退出应用程序时,光标位置在错误的位置,您必须按几次Enter键才能使其回到“干净”提示。
塔希尔·哈桑

@TahirHassan您可以自动提示捕获和清除这里所描述的,但它仍然不是一个完美的解决方案:stackoverflow.com/questions/1305257/...
rkagerer

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

1

我已经写出了一种避免控制台刷新的替代方法。请参阅如何创建既可以用作GUI又可以用作控制台应用程序的Windows程序


1
我对此表示怀疑,但它可以正常工作。就像真的,真的一样完美无瑕。很棒的工作!我见过的第一个真正的问题解决方案。(这是C / C ++代码。不是C#代码。)
B. Nadolson

我同意纳道尔森。这适用于C ++(对于C ++),无需重新启动该过程,也无需多个EXE。
GravityWell,2016年

2
此方法的缺点:(1)完成后,它必须向控制台发送额外的击键;(2)无法将控制台输出重定向到文件;(3)显然尚未通过附加的stdin进行过测试(我猜也不能从文件重定向)。对我来说,要避免立即刷新控制台窗口,这是太多交易了。重新启动方法至少提供了真正的双重控制台/ GUI。我已经向成千上万的用户分发了这样的应用程序,但是还没有收到关于瞬时刷新控制台窗口的任何投诉或评论。
willus

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.