2009-04-30 32 views
44

¿Hay alguna manera de iniciar una aplicación C# con las siguientes características?¿Cómo creo una aplicación C# que decide si mostrarse como consola o aplicación de ventana?

  1. determina por los parámetros de línea de comandos si se trata de una ventana o una consola de aplicación
  2. No muestra una consola cuando se le pide que se de ventana y no muestra una ventana de interfaz gráfica de usuario cuando se está ejecutando desde la consola.

Por ejemplo, habría

myapp.exe /help
de salida a la salida estándar de la consola que utiliza, pero
myapp.exe
por sí mismo sería lanzar mis Winforms o WPF interfaz de usuario.

Las mejores respuestas que conozco hasta ahora implican tener dos ejecutables por separado y usar IPC, pero eso se siente muy raro.


¿Qué opciones tengo y puedo hacer concesiones para obtener el comportamiento descrito en el ejemplo anterior? También estoy abierto a ideas que sean específicas de Winform o específicas de WPF.

Respuesta

57

Convierta la aplicación en una aplicación de Windows normal y cree una consola sobre la marcha si es necesario.

Más detalles en this link (código de abajo desde allí)

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("Argument{0} = {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); 

    } 
} 
+1

¿No crea esto una nueva ventana de consola? Pensé que el problema original era enviar a la "consola que usaste" cuando el usuario escribe MyApp.exe/help. – MichaC

+0

Tiene razón: para conectarse a una consola existente, debe usar AttachConsole (-1). Código actualizado para reflejar eso. –

+9

El mayor problema con este enfoque parece ser que el proceso vuelve inmediatamente a la consola de lanzamiento cuando se inicia desde un shell. En ese caso, el stdout comienza a escribir sobre el resto del texto a medida que usa ese shell/consola. Sería muy útil si hubiera una manera de evitar eso, también, pero aún no he encontrado nada. – Matthew

7

Escriba dos aplicaciones (una consola, una ventana) y luego escriba otra aplicación más pequeña que según los parámetros proporcionados abra una de las otras aplicaciones (y luego se cerraría porque ya no sería necesaria).

+0

Este enfoque me parece el menos hacky. Usted tiene una clara separación de preocupaciones y mantiene las cosas delgadas y simples. – AndyM

1

Tal vez esto link le dará una idea de lo que está buscando hacer.

4

NOTA: No he probado esto, pero creo que funcionaría ...

Usted puede hacer esto:

que su aplicación sea una aplicación Windows Forms. Si recibe una solicitud de consola, no muestre su formulario principal. En su lugar, utilice la invocación de plataforma para llamar al Console Functions en la API de Windows y asigne una consola sobre la marcha.

(otra alternativa es utilizar la API para ocultar la consola en una aplicación de consola, pero es probable que veríamos la consola "parpadeo", ya que fue creado en este caso ...)

+0

+1, y Eric P agregó un código de muestra – Lucas

1

Una forma de hacer esto es escribir una aplicación de ventana que no muestra una ventana si los argumentos de la línea de comando indican que no debería.

Siempre puede obtener los argumentos de la línea de comando y verificarlos antes de mostrar la primera ventana.

+0

, pero ¿qué hay de abrir la ventana de la consola cuando no se muestra una ventana? – Lucas

+0

abre cmd.exe? –

2

Por lo que yo sé, hay una bandera en el exe que dice si se ejecuta como consola o aplicación de ventana. Puede pasar la bandera con herramientas que vienen con Visual Studio, pero no puede hacerlo en tiempo de ejecución.

Si el exe se compila como una consola, siempre abrirá una nueva consola si no se inicia desde una. Si el exe es una aplicación, no puede enviar a la consola. Puede generar una consola separada, pero no se comportará como una aplicación de consola.

Yo el pasado hemos usado 2 exe's separados. La consola es una envoltura delgada sobre los formularios uno (puede hacer referencia a un exe como haría referencia a un dll, y puede usar el atributo [assembly: InternalsVisibleTo ("cs_friend_assemblies_2")] para confiar en la consola uno, así que no lo hace t tiene que exponer más de lo necesario).

5

he hecho esto mediante la creación de dos aplicaciones por separado.

Crea la aplicación WPF con este nombre: MyApp.exe. Y cree la aplicación de consola con este nombre: MyApp.com. Cuando escriba el nombre de su aplicación en la línea de comandos como esta MyApp o MyApp /help (sin la extensión .exe), prevalecerá la aplicación de la consola con la extensión .com. Puede hacer que su aplicación de consola invoque el MyApp.exe según los parámetros.

Así es exactamente como se comporta devenv. Al escribir devenv en la línea de comando se ejecutará el IDE de Visual Studio. Si pasa parámetros como /build, permanecerá en la línea de comando.

+0

Sí, esta es la forma de la vieja escuela que he usado en el pasado. Esperaba otro enfoque. Gracias, sin embargo. – Matthew

+0

No he podido encontrar cómo configurar la extensión 'com'. Si cambio 'Assembly name' en la configuración del proyecto, genera' MyApp.com.exe' – itsho

+0

¿Cómo se obtiene MyApp.com? – FlyingMaverick

0

No 1 es fácil.

No 2 no se puede hacer, no creo.

Los documentos decir:

Las llamadas a métodos como la escritura y WriteLine no tienen ningún efecto en las aplicaciones de Windows.

La clase System.Console se inicializa de manera diferente en las aplicaciones de consola y GUI. Puede verificar esto mirando la clase de la consola en el depurador en cada tipo de aplicación. No estoy seguro de si hay alguna forma de reiniciarlo.

Demostración: Crear una nueva aplicación de Windows Forms, a continuación, reemplace el método principal con esto:

static void Main(string[] args) 
    { 
     if (args.Length == 0) 
     { 
      Application.EnableVisualStyles(); 
      Application.SetCompatibleTextRenderingDefault(false); 
      Application.Run(new Form1()); 
     } 
     else 
     { 
      Console.WriteLine("Console!\r\n"); 
     } 
    } 

La idea es que los parámetros de línea de comandos se imprimirán en la consola y salir. Cuando lo ejecutas sin argumentos, obtienes la ventana. Pero cuando lo ejecuta con un argumento de línea de comando, no pasa nada.

A continuación, seleccione las propiedades del proyecto, cambie el tipo de proyecto a "Aplicación de consola" y vuelva a compilar. Ahora cuando lo ejecutas con un argumento, obtienes "Console!" como quieras. Y cuando lo ejecuta (desde la línea de comando) sin argumentos, obtiene la ventana. Pero el símbolo del sistema no volverá hasta que salga del programa. Y si ejecuta el programa desde Explorer, se abrirá una ventana de comando y luego obtendrá una ventana.

2

Crearía una solución que es una aplicación de Windows Form ya que hay dos funciones que puede llamar que se engancharán en la consola actual. Entonces puede tratar el programa como un programa de consola. o de forma predeterminada, puede iniciar la GUI.

La función AttachConsole no creará una nueva consola. Para obtener más información acerca de AttachConsole, consulte PInvoke: AttachConsole

A continuación, encontrará un programa de ejemplo sobre cómo usarlo.

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

básicamente yo que la manera representada en la respuesta de Eric, además, que retire la consola con FreeConsole y el uso de los comando SendKeys para obtener el símbolo del sistema de atrás.

[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()); 

    } 
+3

Esta debería ser la respuesta. SendKeys es importante. – derFunk

1

Lo importante para recordar a hacer después de AttachConsole()AllocConsole() o llamadas a conseguir que funcione en todos los casos es:

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

he encontrado que funciona con o sin proceso de alojamiento VS. Con la salida que se envía con System.Console.WriteLine o System.Console.out.WriteLine antes de llamar a AttachConsole o AllocConsole. He incluido mi método a continuación:

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

También llamé a esto cuando yo estaba hecho por si necesitaba símbolo del sistema para volver a mostrar cuando estaba hecho haciendo de salida.

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

Espero que esto ayude ...

0

he encontrado una forma de hacer esto incluyendo el uso de la entrada estándar, pero debo advertir que no es bonita.

El problema con el uso de stdin desde una consola conectada es que el shell también leerá de él. Esto hace que la entrada a veces vaya a su aplicación pero a veces al shell.

La solución es bloquear el shell durante la vida útil de las aplicaciones (aunque técnicamente podría intentar bloquearlo solo cuando lo necesite). La forma en que elijo hacer esto es enviando las teclas al intérprete de comandos para ejecutar un comando de PowerShell que espera a que finalice la aplicación.

Dicho sea de paso, esto también soluciona el problema de que el aviso no regrese después de que la aplicación finaliza.

He tratado brevemente de hacer que funcione desde la consola de powershell también. Se aplican los mismos principios, pero no pude ejecutar mi comando. Es posible que powershell tenga algunas comprobaciones de seguridad para evitar la ejecución de comandos desde otras aplicaciones. Debido a que no uso mucho Powell, no lo investigué.

[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(); 
    } 
Cuestiones relacionadas