Kann eine ausführbare Datei sowohl eine Konsole als auch eine GUI-Anwendung sein?

80

Ich möchte ein C # -Programm erstellen, das je nach den übergebenen Flags als CLI- oder GUI-Anwendung ausgeführt werden kann. Kann das gemacht werden?

Ich habe diese verwandten Fragen gefunden, aber sie decken meine Situation nicht genau ab:

BCS
quelle
1
Nur zur Veranschaulichung: Es hängt wirklich mit dem Betriebssystem zusammen, nicht mit CLR. Mit Mono unter Linux ist es beispielsweise kein Problem, eine solche Anwendung zu erstellen (tatsächlich ist jede Anwendung eine Konsole, kann aber auch mit Windows alles tun) - genau wie mit Java oder einem anderen * nix-Programm. Ein gängiges Muster ist die Anmeldung an der Konsole, während die Benutzeroberfläche für den Benutzer verwendet wird.
konrad.kruczynski

Antworten:

98

Die Antwort von Jdigital verweist auf Raymond Chens Blog , in dem erklärt wird, warum Sie keine Anwendung haben können, die sowohl ein Konsolenprogramm als auch ein Nicht-Konsolenprogramm ist *: Das Betriebssystem muss wissen, bevor das Programm ausgeführt wird, welches Subsystem verwendet werden soll. Sobald das Programm gestartet wurde, ist es zu spät, um den anderen Modus anzufordern.

Die Antwort von Cade verweist auf einen Artikel über das Ausführen einer .NET WinForms-Anwendung mit einer Konsole . Es verwendet die Technik des Aufrufs, AttachConsolenachdem das Programm gestartet wurde. Dies ermöglicht es dem Programm, in das Konsolenfenster der Eingabeaufforderung, die das Programm gestartet hat, zurückzuschreiben. Die Kommentare in diesem Artikel zeigen jedoch, was ich als schwerwiegenden Fehler betrachte: Der untergeordnete Prozess steuert die Konsole nicht wirklich. Die Konsole akzeptiert weiterhin Eingaben im Namen des übergeordneten Prozesses, und dem übergeordneten Prozess ist nicht bekannt, dass er warten sollte, bis das untergeordnete Element beendet ist, bevor die Konsole für andere Zwecke verwendet wird.

Chens Artikel verweist auf einen Artikel von Junfeng Zhang, der einige andere Techniken erklärt .

Das erste ist, was devenv verwendet. Es funktioniert, indem tatsächlich zwei Programme vorhanden sind. Eines ist devenv.exe , das Haupt-GUI-Programm, und das andere ist devenv.com , das Aufgaben im Konsolenmodus behandelt. Wenn es jedoch nicht konsolenähnlich verwendet wird, leitet es seine Aufgaben an devenv.exe und weiter Ausgänge. Die Technik basiert auf der Win32-Regel, dass COM- Dateien vor Exe- Dateien ausgewählt werden, wenn Sie einen Befehl ohne Dateierweiterung eingeben.

Es gibt eine einfachere Variante, die der Windows Script Host ausführt. Es bietet zwei vollständig separate Binärdateien, wscript.exe und cscript.exe . Ebenso bietet Java java.exe für Konsolenprogramme und javaw.exe für Nicht-Konsolenprogramme.

Junfengs zweite Technik ist die, die ildasm verwendet. Er zitiert den Prozess, den der Autor von ildasm durchlaufen hat, als er es in beiden Modi ausgeführt hat. Letztendlich ist hier, was es tut:

  1. Das Programm ist als Binärdatei im Konsolenmodus markiert und beginnt daher immer mit einer Konsole. Dadurch kann die Eingangs- und Ausgangsumleitung wie gewohnt funktionieren.
  2. Wenn das Programm keine Befehlszeilenparameter im Konsolenmodus hat, wird es selbst neu gestartet.

Es reicht nicht aus, einfach aufzurufen FreeConsole, damit die erste Instanz kein Konsolenprogramm mehr ist. Dies liegt daran, dass der Prozess, der das Programm gestartet hat, cmd.exe , "weiß", dass er ein Programm im Konsolenmodus gestartet hat und darauf wartet, dass das Programm nicht mehr ausgeführt wird. Durch das Aufrufen FreeConsolewürde ildasm die Verwendung der Konsole beenden, aber der übergeordnete Prozess würde die Verwendung der Konsole nicht starten .

Die erste Instanz startet sich also neu (mit einem zusätzlichen Befehlszeilenparameter, nehme ich an). Wenn Sie anrufen CreateProcess, gibt es zwei verschiedene Fahnen , um zu versuchen, DETACHED_PROCESSundCREATE_NEW_CONSOLE , von denen dafür sorgen, dass die zweite Instanz nicht an die übergeordnete Konsole angebracht werden. Danach kann die erste Instanz beendet werden und die Eingabeaufforderung kann die Verarbeitungsbefehle fortsetzen.

Der Nebeneffekt dieser Technik ist, dass beim Starten des Programms über eine GUI-Oberfläche immer noch eine Konsole vorhanden ist. Es blinkt kurz auf dem Bildschirm und verschwindet dann.

Der Teil in Junfengs Artikel über die Verwendung von editbin zum Ändern des Konsolenmodus-Flags des Programms ist meiner Meinung nach ein roter Hering. Ihr Compiler oder Ihre Entwicklungsumgebung sollte eine Einstellung oder Option bereitstellen, um zu steuern, welche Art von Binärdatei erstellt wird. Es sollte nicht nötig sein, danach etwas zu ändern.

Die Quintessenz ist also, dass Sie entweder zwei Binärdateien oder ein kurzes Flackern eines Konsolenfensters haben können . Sobald Sie sich für das kleinere Übel entschieden haben, haben Sie die Wahl zwischen Implementierungen.

*Ich sage Nicht-Konsole statt GUI, weil es sonst eine falsche Zweiteilung ist. Nur weil ein Programm keine Konsole hat, heißt das nicht, dass es eine GUI hat. Eine Dienstanwendung ist ein Paradebeispiel. Ein Programm kann auch eine Konsole und Fenster haben.

Rob Kennedy
quelle
Ich weiß, dass dies eine alte Antwort ist, aber ich glaube, dass der Zweck dieses Tricks darin besteht, die CRT dazu zu bringen, eine WinMainFunktion mit geeigneten Parametern zu verknüpfen (also kompilieren mit /SUBSYSTEM:WINDOWS) und dann den Modus nachträglich zu ändern Der Loader startet einen Konsolenhost. Für mehr Feedback habe ich dies mit CREATE_NO_WINDOWin CreateProcess versucht und GetConsoleWindow() == NULLals meine Überprüfung, ob ein Relaunch durchgeführt wurde oder nicht. Dies behebt das Flackern der Konsole nicht, bedeutet jedoch, dass kein spezielles cmd-Argument vorhanden ist.
Dies ist eine großartige Antwort, aber der Vollständigkeit halber lohnt es sich wahrscheinlich, die Hauptunterschiede zwischen einem Konsolen- und einem Nicht-Konsolen-Programm anzugeben (Missverständnisse hier scheinen zu vielen der folgenden falschen Antworten zu führen). Das heißt: Eine Konsolen-App, die von der Konsole aus gestartet wird, gibt die Steuerung erst nach Abschluss an die übergeordnete Konsole zurück, während eine GUI-App sich teilt und sofort zurückkehrt. Wenn Sie sich nicht sicher sind, können Sie DUMPBIN / headers verwenden und nach der SUBSYSTEM-Zeile suchen, um genau zu sehen, welchen Geschmack Sie haben.
Piers7
Dies ist eine veraltete beste Antwort. Zumindest aus C / C ++ - Perspektive. Siehe Dantills Lösung unten für Win32, das wahrscheinlich von jemandem an C # angepasst werden könnte.
B. Nadolson
1
Ich halte diese Antwort nicht für veraltet. Die Methode funktioniert gut und die Bewertung der Antwort spricht für sich. Dantills Ansatz trennt stdin von der Konsolen-App. Ich habe unten eine C-Version von Kennedys "Momentary Flicker" -Ansatz als separate Antwort bereitgestellt (ja, ich weiß, OP hat über C # gepostet). Ich habe es mehrmals benutzt und bin ziemlich zufrieden damit.
Willus
Sie könnten dies in Java tun ..)
Antoniossss
11

Lesen Sie den Blog von Raymond zu diesem Thema:

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

Sein erster Satz: "Sie können nicht, aber Sie können versuchen, es zu fälschen."

jdigital
quelle
.Net macht es eigentlich ziemlich einfach, es zu "fälschen", aber diese Antwort ist technisch korrekt.
Joel Coehoorn
6

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

Überprüfen Sie einfach die Befehlszeilenargumente vor dem WinForms- Application.Material.

Ich sollte hinzufügen, dass es in .NET lächerlich einfach ist, einfach eine Konsole und GUI-Projekte in derselben Lösung zu erstellen, die alle ihre Assemblys außer main gemeinsam nutzen. In diesem Fall können Sie festlegen, dass die Befehlszeilenversion einfach die GUI-Version startet, wenn sie ohne Parameter gestartet wird. Sie würden eine blinkende Konsole bekommen.

Cade Roux
quelle
Das Vorhandensein von Befehlszeilenparametern ist kaum ein sicherer Hinweis auf einen Brand. Viele Windows-Apps können Befehlszeilenparameter übernehmen
Neil N
3
Mein Punkt war, wenn es keine gibt, starten Sie die GUI-Version. Wenn Sie möchten, dass die GUI-Version mit Parametern gestartet wird, können Sie vermutlich einen Parameter dafür haben.
Cade Roux
5

Es gibt eine einfache Möglichkeit, das zu tun, was Sie wollen. Ich benutze es immer, wenn ich Apps schreibe, die sowohl eine CLI als auch eine GUI haben sollten. Sie müssen Ihren "OutputType" auf "ConsoleApplication" setzen, damit dies funktioniert.

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
        });
      }
    }
  }
user1566352
quelle
1
Ich liebe dies und es funktioniert gut auf meinem Windows 7-Entwicklungscomputer. Ich habe jedoch einen (virtuellen) Windows XP-Computer und es scheint, dass der neu gestartete Prozess immer eine Konsole erhält und so in einer Endlosschleife verschwindet, die sich selbst neu startet. Irgendwelche Ideen?
Simon Hewitt
1
Seien Sie sehr vorsichtig damit, unter Windows XP führt dies tatsächlich zu einer unbegrenzten Respawn-Schleife, die sehr schwer zu töten ist.
Benutzer
3

Ich denke, die bevorzugte Technik ist die von Rob als Devenv bezeichnete Technik, bei der zwei ausführbare Dateien verwendet werden: ein Launcher ".com" und die ursprüngliche ".exe". Dies ist nicht so schwierig zu verwenden, wenn Sie den Boilerplate-Code haben, mit dem Sie arbeiten können (siehe Link unten).

Die Technik verwendet Tricks, um ".com" als Proxy für stdin / stdout / stderr zu verwenden und die gleichnamige EXE-Datei zu starten. Dies gibt das Verhalten, dass das Programm beim Aufrufen über eine Konsole in einem Befehlszeilenmodus ausgeführt werden kann (möglicherweise nur, wenn bestimmte Befehlszeilenargumente erkannt werden), während es weiterhin als konsolenfreie GUI-Anwendung gestartet werden kann.

Ich habe ein Projekt namens dualsubsystem auf Google Code gehostet , das eine alte Codeguru-Lösung dieser Technik aktualisiert und den Quellcode und die funktionierenden Beispiel-Binärdateien bereitstellt.

gabeiscoding
quelle
3

Hier ist meiner Meinung nach die einfache .NET C # -Lösung für das Problem. Um das Problem noch einmal zu wiederholen: Wenn Sie die Konsolen- "Version" der App über eine Befehlszeile mit einem Schalter ausführen, wartet die Konsole weiter (sie kehrt nicht zur Eingabeaufforderung zurück und der Prozess läuft weiter), selbst wenn Sie eine haben Environment.Exit(0)am Ende Ihres Codes. Um dies zu beheben Environment.Exit(0), rufen Sie kurz vor dem Anruf Folgendes auf:

SendKeys.SendWait("{ENTER}");

Dann erhält die Konsole die letzte Eingabetaste, die sie benötigt, um zur Eingabeaufforderung zurückzukehren, und der Vorgang wird beendet. Hinweis: Rufen Sie nicht an SendKeys.Send(), da die App sonst abstürzt.

Es ist immer noch notwendig, AttachConsole()wie in vielen Beiträgen erwähnt anzurufen , aber damit bekomme ich kein Startfensterflimmern, wenn ich die WinForm-Version der App starte.

Hier ist der gesamte Code in einer Beispiel-App, die ich erstellt habe (ohne den WinForms-Code):

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

Hoffe, es hilft jemandem, auch Tage mit diesem Problem zu verbringen. Danke für den Hinweis, gehe zu @dantill.

LTDev
quelle
Ich habe dies versucht und das Problem ist, dass alles, was mit geschrieben wurde, den Console.WriteLineTextcursor der (übergeordneten) Konsole nicht vorschiebt. Wenn Sie die App beenden, befindet sich die Cursorposition an der falschen Stelle und Sie müssen einige Male die Eingabetaste drücken, um zu einer "sauberen" Eingabeaufforderung zurückzukehren.
Tahir Hassan
@TahirHassan Sie können die Erfassung und Bereinigung von Eingabeaufforderungen wie hier beschrieben automatisieren, aber es ist immer noch keine perfekte Lösung: 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
quelle
1

Ich habe einen alternativen Ansatz geschrieben, der den Konsolenblitz vermeidet. Siehe So erstellen Sie ein Windows-Programm, das sowohl als GUI- als auch als Konsolenanwendung funktioniert .

Dantill
quelle
1
Ich war skeptisch, aber es funktioniert einwandfrei. Wie wirklich, wirklich einwandfrei. Exzellente Arbeit! Erste echte Lösung, die ich gesehen habe. (Es ist C / C ++ - Code. Nicht C # -Code.)
B. Nadolson
Ich stimme B. Nadolson zu. Dies funktioniert (für C ++), ohne den Prozess neu zu starten und ohne mehrere EXE-Dateien.
GravityWell
2
Nachteile dieser Methode: (1) Sie muss einen zusätzlichen Tastendruck an die Konsole senden, wenn sie fertig ist, (2) sie kann die Konsolenausgabe nicht in eine Datei umleiten und (3) sie wurde anscheinend nicht mit angehängtem stdin getestet (was Ich würde auch vermuten, dass es nicht von einer Datei umgeleitet werden kann. Für mich sind das zu viele Trades, um zu vermeiden, dass ein Konsolenfenster vorübergehend aufleuchtet. Die Neustartmethode bietet zumindest eine echte Dual-Konsole / GUI. Ich habe eine solche App an Zehntausende von Benutzern verteilt und keine einzige Beschwerde oder einen Kommentar zu dem momentan blinkenden Konsolenfenster erhalten.
Willus
0

Führen Sie AllocConsole () in einem statischen Konstruktor aus, der für mich funktioniert

Zottelig
quelle