¿Puede un ejecutable ser tanto una consola como una aplicación GUI?

81

Quiero crear un programa en C # que se pueda ejecutar como una aplicación CLI o GUI dependiendo de las banderas que se le pasen. Se puede hacer esto?

Encontré estas preguntas relacionadas, pero no cubren exactamente mi situación:

BCS
fuente
1
Solo para que conste: realmente está relacionado con el sistema operativo, no con CLR. Por ejemplo, con Mono en Linux no hay problemas para crear dicha aplicación (de hecho, cada aplicación es una consola, pero también puede hacer lo que sea con Windows), al igual que con Java o cualquier otro programa * nix. Y el patrón común es iniciar sesión en la consola mientras se usa la GUI para el usuario.
konrad.kruczynski

Respuestas:

99

La respuesta de Jdigital apunta al blog de Raymond Chen , que explica por qué no puede tener una aplicación que sea a la vez un programa de consola y un programa que no sea de consola *: el sistema operativo necesita saber antes de que el programa comience a ejecutarse qué subsistema usar. Una vez que el programa ha comenzado a ejecutarse, es demasiado tarde para volver atrás y solicitar el otro modo.

La respuesta de Cade apunta a un artículo sobre cómo ejecutar una aplicación .Net WinForms con una consola . Utiliza la técnica de llamar AttachConsoledespués de que el programa comienza a ejecutarse. Esto tiene el efecto de permitir que el programa vuelva a escribir en la ventana de la consola del símbolo del sistema que inició el programa. Pero los comentarios en ese artículo señalan lo que considero un defecto fatal: el proceso hijo no controla realmente la consola. La consola continúa aceptando entradas en nombre del proceso principal, y el proceso principal no es consciente de que debe esperar a que el hijo termine de ejecutarse antes de usar la consola para otras cosas.

El artículo de Chen apunta a un artículo de Junfeng Zhang que explica un par de otras técnicas .

El primero es lo que usa Devenv . Funciona al tener dos programas. Uno es devenv.exe , que es el programa GUI principal, y el otro es devenv.com , que maneja tareas en modo consola, pero si se usa de una manera no similar a una consola, reenvía sus tareas a devenv.exe y salidas. La técnica se basa en la regla Win32 de que los archivos com se eligen antes que los archivos exe cuando se escribe un comando sin la extensión del archivo.

Hay una variación más simple de esto que hace Windows Script Host. Proporciona dos binarios completamente separados, wscript.exe y cscript.exe . Asimismo, Java proporciona java.exe para programas de consola y javaw.exe para programas que no son de consola.

La segunda técnica de Junfeng es la que usa ildasm . Cita el proceso por el que pasó el autor de ildasm al ejecutarlo en ambos modos. En última instancia, esto es lo que hace:

  1. El programa está marcado como un binario en modo consola, por lo que siempre comienza con una consola. Esto permite que la redirección de entrada y salida funcione con normalidad.
  2. Si el programa no tiene parámetros de línea de comandos en modo consola, se reinicia.

No es suficiente simplemente llamar FreeConsolepara que la primera instancia deje de ser un programa de consola. Esto se debe a que el proceso que inició el programa, cmd.exe , "sabe" que inició un programa en modo consola y está esperando que el programa deje de ejecutarse. Llamar FreeConsoleharía que ildasm dejara de usar la consola, pero no haría que el proceso principal comenzara a usar la consola.

Entonces, la primera instancia se reinicia sola (con un parámetro de línea de comando adicional, supongo). Cuando llame CreateProcess, hay dos banderas diferentes para probar, DETACHED_PROCESSyCREATE_NEW_CONSOLE cualquiera de las cuales garantizará que la segunda instancia no se adjunte a la consola principal. Después de eso, la primera instancia puede terminar y permitir que el símbolo del sistema reanude el procesamiento de los comandos.

El efecto secundario de esta técnica es que cuando inicia el programa desde una interfaz GUI, todavía habrá una consola. Destellará en la pantalla momentáneamente y luego desaparecerá.

Creo que la parte del artículo de Junfeng sobre el uso de editbin para cambiar el indicador del modo de consola del programa es una pista falsa. Su compilador o entorno de desarrollo debe proporcionar una configuración u opción para controlar qué tipo de binario crea. No debería ser necesario modificar nada después.

La conclusión, entonces, es que puede tener dos binarios, o puede tener un parpadeo momentáneo de una ventana de consola . Una vez que decida cuál es el mal menor, puede elegir las implementaciones.

*Digo sin consola en lugar de GUI porque de lo contrario es una dicotomía falsa. El hecho de que un programa no tenga una consola no significa que tenga una GUI. Una aplicación de servicio es un buen ejemplo. Además, un programa puede tener una consola y ventanas.

Rob Kennedy
fuente
Sé que esta es una respuesta antigua, pero en los puntos de pista falsa sobre editbin, creo que el propósito de ese truco es hacer que el CRT vincule una WinMainfunción con los parámetros apropiados (así que compile /SUBSYSTEM:WINDOWS) y luego cambie el modo ex post facto para el cargador lanza un host de consola. Para obtener más comentarios, probé esto CREATE_NO_WINDOWen CreateProcess y GetConsoleWindow() == NULLcomo mi check-if-relanzado o no. Esto no corrige el parpadeo de la consola, pero significa que no tiene un argumento cmd especial.
Esta es una gran respuesta, pero para que sea completa, probablemente valga la pena indicar cuáles son las principales diferencias entre una consola y un programa que no es de consola (un malentendido aquí parece llevar a muchas de las respuestas erróneas a continuación). Es decir: una aplicación de consola, iniciada desde la consola, no devolverá el control a la consola principal hasta que se complete, mientras que una aplicación GUI se bifurcará y regresará inmediatamente. Cuando no esté seguro, puede usar DUMPBIN / encabezados y buscar la línea SUBSYSTEM para ver exactamente qué sabor tiene.
embarcaderos 7
Esta es una mejor respuesta obsoleta. Al menos desde una perspectiva C / C ++. Consulte la solución de dantill a continuación para Win32, que probablemente alguien podría adaptar a C #.
B. Nadolson
1
No considero esta respuesta obsoleta. El método funciona bien y la calificación de la respuesta habla por sí sola. El enfoque de Dantill desconecta stdin de la aplicación de consola. He proporcionado una versión C del enfoque de "parpadeo momentáneo" de Kennedy a continuación como una respuesta separada (sí, lo sé, OP publicó sobre C #). Lo he usado varias veces y estoy bastante contento con él.
willus
Podrías hacer esto en Java ..)
Antoniossss
6

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

Simplemente verifique los argumentos de la línea de comando antes de las Application.cosas de WinForms .

Debo agregar que en .NET es RIDÍCULAMENTE fácil simplemente hacer una consola y proyectos GUI en la misma solución que comparten todos sus ensamblajes excepto main. Y en este caso, puede hacer que la versión de línea de comandos simplemente inicie la versión de GUI si se inicia sin parámetros. Obtendría una consola intermitente.

Cade Roux
fuente
La existencia de parámetros de línea de comando no es una indicación segura. Muchas aplicaciones de Windows pueden tomar parámetros de línea de comandos
Neil N
3
Mi punto fue que si no hay ninguno, inicie la versión GUI. Si desea que la versión de GUI se inicie con parámetros, presumiblemente puede tener un parámetro para eso.
Cade Roux
5

Hay una forma sencilla de hacer lo que quiera. Siempre lo uso al escribir aplicaciones que deberían tener una CLI y una GUI. Debe establecer su "OutputType" en "ConsoleApplication" para que esto funcione.

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
        });
      }
    }
  }
usuario1566352
fuente
1
Me encanta esto y funciona bien en mi máquina de desarrollo de Windows 7. Sin embargo, tengo una máquina (virtual) de Windows XP y parece que el proceso reiniciado siempre obtiene una consola y, por lo tanto, desaparece en un bucle sin fin reiniciándose. ¿Algunas ideas?
Simon Hewitt
1
Tenga mucho cuidado con esto, en Windows XP esto conduce a un ciclo de reaparición ilimitado que es muy difícil de eliminar.
usuario
3

Creo que la técnica preferida es lo que Rob llamó la técnica devenv de usar dos ejecutables: un lanzador ".com" y el original ".exe". Esto no es tan complicado de usar si tiene el código estándar para trabajar (vea el enlace a continuación).

La técnica utiliza trucos para que ".com" sea un proxy para stdin / stdout / stderr y ejecute el archivo .exe del mismo nombre. Esto da el comportamiento de permitir que el programa se ejecute en un modo de línea de comandos cuando se llama desde una consola (potencialmente solo cuando se detectan ciertos argumentos de la línea de comandos) mientras aún se puede iniciar como una aplicación GUI sin una consola.

Presenté un proyecto llamado dualsubsystem en Google Code que actualiza una antigua solución de codeguru de esta técnica y proporciona el código fuente y binarios de ejemplo de trabajo.

gabeiscoding
fuente
3

Esto es lo que creo que es la solución simple de .NET C # al problema. Solo para reafirmar el problema, cuando ejecuta la "versión" de la consola de la aplicación desde una línea de comando con un interruptor, la consola sigue esperando (no regresa al símbolo del sistema y el proceso sigue ejecutándose) incluso si tiene un Environment.Exit(0)al final de su código. Para solucionar esto, justo antes de llamar Environment.Exit(0), llame a esto:

SendKeys.SendWait("{ENTER}");

Luego, la consola obtiene la tecla Enter final que necesita para regresar al símbolo del sistema y el proceso finaliza. Nota: No llames SendKeys.Send()o la aplicación se bloqueará.

Todavía es necesario llamar AttachConsole()como se mencionó en muchas publicaciones, pero con esto no obtengo ningún parpadeo en la ventana de comando al iniciar la versión WinForm de la aplicación.

Aquí está el código completo en una aplicación de muestra que creé (sin el código de 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());
            }
        }
    }
}

Espero que ayude a alguien a pasar días en este problema. Gracias por la pista, vaya a @dantill.

LTDev
fuente
Intenté esto y el problema es que cualquier cosa escrita con Console.WriteLineno avanza el cursor de texto de la consola (principal). Entonces, cuando la aplicación se cierra, la posición del cursor está en el lugar equivocado y debe presionar Intro varias veces solo para que vuelva a un mensaje "limpio".
Tahir Hassan
@TahirHassan Puede automatizar la captura y limpieza de mensajes como se describe aquí, pero aún no es una solución perfecta: 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);
    }
Willus
fuente
1

He escrito un enfoque alternativo que evita el flash de la consola. Consulte Cómo crear un programa de Windows que funcione como aplicación de consola y GUI .

dantill
fuente
1
Era escéptico pero funciona a la perfección. Realmente, realmente impecable. ¡Excelente trabajo! Primera solución verdadera al problema que he visto. (Es código C / C ++. No código C #.)
B. Nadolson
Estoy de acuerdo con B. Nadolson. Esto funciona (para C ++), sin relanzar el proceso y sin múltiples EXE.
GravityWell
2
Inconvenientes de este método: (1) tiene que enviar una pulsación de tecla adicional a la consola cuando está hecho, (2) no puede redirigir la salida de la consola a un archivo y (3) aparentemente no se ha probado con stdin adjunto (que Supongo que tampoco se puede redirigir desde un archivo). Para mí, son demasiados intercambios solo para evitar que aparezca momentáneamente una ventana de consola. El método de relanzamiento proporciona al menos una verdadera consola dual / GUI. He distribuido una aplicación de este tipo a decenas de miles de usuarios y no he recibido una sola queja o comentario sobre la ventana de la consola que parpadea momentáneamente.
willus
0

Ejecutar AllocConsole () en un constructor estático funciona para mí

Lanudo
fuente