Czy jeden plik wykonywalny może być zarówno konsolą, jak i aplikacją GUI?

81

Chcę utworzyć program C # , który można uruchomić jako aplikację CLI lub GUI w zależności od tego, jakie flagi są do niego przekazywane. Czy można to zrobić?

Znalazłem te powiązane pytania, ale nie obejmują one dokładnie mojej sytuacji:

BCS
źródło
1
Tak dla przypomnienia: tak naprawdę jest związany z systemem operacyjnym, a nie z CLR. Na przykład z Mono na Linuksie nie ma problemu z utworzeniem takiej aplikacji (tak naprawdę każda aplikacja jest konsolą, ale może też robić cokolwiek z Windowsem) - tak jak z Javą czy każdym innym programem * nix. Powszechnym wzorcem jest logowanie się na konsoli podczas korzystania z GUI dla użytkownika.
konrad.kruczynski

Odpowiedzi:

99

Odpowiedź Jdigital wskazuje na blog Raymonda Chena , który wyjaśnia, dlaczego nie można mieć aplikacji, która jest jednocześnie programem konsolowym i innym niż konsolowy *: system operacyjny musi wiedzieć, zanim program zacznie działać, którego podsystemu ma używać. Po uruchomieniu programu jest już za późno, aby wrócić i zażądać innego trybu.

Odpowiedź Cade'a wskazuje na artykuł o uruchamianiu aplikacji .Net WinForms z konsolą . Używa techniki wywoływania AttachConsolepo uruchomieniu programu. W efekcie program może zapisywać z powrotem w oknie konsoli wiersza polecenia, który uruchomił program. Ale komentarze w tym artykule wskazują na to, co uważam za fatalną wadę: proces potomny tak naprawdę nie kontroluje konsoli. Konsola nadal przyjmuje dane wejściowe w imieniu procesu nadrzędnego, a proces nadrzędny nie jest świadomy tego, że powinien czekać na zakończenie działania dziecka, zanim użyje konsoli do innych celów.

Artykuł Chena wskazuje na artykuł Junfeng Zhang, który wyjaśnia kilka innych technik .

Pierwszym jest to, czego używa devenv . Działa poprzez faktyczne posiadanie dwóch programów. Jeden to devenv.exe , który jest głównym programem graficznym, a drugi to devenv.com , który obsługuje zadania w trybie konsoli, ale jeśli jest używany w sposób inny niż konsola, przekazuje swoje zadania do devenv.exe i wyjścia. Technika ta opiera się na regule Win32, zgodnie z którą pliki com są wybierane przed plikami exe po wpisaniu polecenia bez rozszerzenia pliku.

Istnieje prostsza odmiana tego, którą robi host skryptów systemu Windows. Udostępnia dwa całkowicie oddzielne pliki binarne, wscript.exe i cscript.exe . Podobnie Java udostępnia java.exe dla programów konsolowych i javaw.exe dla programów innych niż konsole.

Drugą techniką Junfenga jest ildasm . Cytuje proces, przez który przeszedł autor ildasm , uruchamiając go w obu trybach. Ostatecznie, oto, co robi:

  1. Program jest oznaczony jako plik binarny w trybie konsoli, więc zawsze zaczyna się od konsoli. Dzięki temu przekierowanie wejścia i wyjścia działa normalnie.
  2. Jeśli program nie ma parametrów wiersza poleceń trybu konsoli, uruchamia się ponownie.

Nie wystarczy po prostu wywołać, FreeConsoleaby pierwsza instancja przestała być programem konsolowym. Dzieje się tak, ponieważ proces, który uruchomił program, cmd.exe , „wie”, że uruchomił program w trybie konsoli i czeka, aż program przestanie działać. Wywołanie FreeConsoleuczyniłoby ildasm zaprzestać korzystania z konsoli, ale nie może sprawić, że proces macierzysty uruchomić za pomocą konsoli.

Więc pierwsza instancja uruchamia się ponownie (jak przypuszczam z dodatkowym parametrem wiersza polecenia). Podczas wywołania CreateProcesssą dwie różne flagi do wypróbowania, DETACHED_PROCESSaCREATE_NEW_CONSOLE każda z nich zapewni, że druga instancja nie zostanie podłączona do konsoli nadrzędnej. Następnie pierwsza instancja może zakończyć się i pozwolić wierszowi polecenia na wznowienie przetwarzania poleceń.

Efektem ubocznym tej techniki jest to, że po uruchomieniu programu z interfejsu GUI konsola nadal będzie dostępna. Zacznie migać na ekranie przez chwilę, a następnie zniknie.

Część artykułu Junfenga o używaniu editbin do zmiany flagi trybu konsoli programu jest, jak sądzę, czerwonym śledziem. Twój kompilator lub środowisko programistyczne powinno zapewniać ustawienie lub opcję kontrolowania, jaki rodzaj pliku binarnego tworzy. Nie powinno być potrzeby późniejszych modyfikacji.

Najważniejsze jest więc to, że możesz mieć dwa pliki binarne lub możesz mieć chwilowe migotanie okna konsoli . Kiedy już zdecydujesz, co jest mniejszym złem, masz wybór implementacji.

*Mówię, że nie jest to konsola zamiast GUI, ponieważ w przeciwnym razie jest to fałszywa dychotomia. To, że program nie ma konsoli, nie oznacza, że ​​ma GUI. Najlepszym przykładem jest aplikacja usługowa. Ponadto program może mieć konsolę i okna.

Rob Kennedy
źródło
Wiem, że to stara odpowiedź, ale jeśli chodzi o kwestie związane z editbin, uważam, że celem tej sztuczki jest skłonienie CRT do połączenia WinMainfunkcji z odpowiednimi parametrami (tak skompiluj z /SUBSYSTEM:WINDOWS), a następnie zmiana trybu ex post facto, więc moduł ładujący uruchamia hosta konsoli. Aby uzyskać więcej informacji zwrotnych, wypróbowałem to CREATE_NO_WINDOWw programie CreateProcess i GetConsoleWindow() == NULLjako mój sprawdzanie, czy ponownie uruchomiono lub nie. Nie naprawia to migotania konsoli, ale oznacza brak specjalnego argumentu cmd.
To świetna odpowiedź, ale dla kompletności prawdopodobnie warto wyjaśnić, jakie są główne różnice między konsolą a programem „innym niż konsolowy” (tutaj nieporozumienie wydaje się prowadzić do wielu błędnych odpowiedzi poniżej). To znaczy: aplikacja konsoli, uruchomiona z konsoli, nie zwróci kontroli do konsoli nadrzędnej, dopóki nie zostanie zakończona, podczas gdy aplikacja GUI zostanie rozwidlona i natychmiast wróci. Jeśli nie jesteś pewien, możesz użyć DUMPBIN / nagłówków i poszukać linii SUBSYSTEM, aby zobaczyć dokładnie, jaki masz smak.
mola
To jest przestarzała najlepsza odpowiedź. Przynajmniej z perspektywy C / C ++. Zobacz poniżej rozwiązanie firmy Dantill dla Win32, które prawdopodobnie może zostać przez kogoś dostosowane do C #.
B. Nadolson
1
Nie uważam tej odpowiedzi za przestarzałą. Metoda działa dobrze, a ocena odpowiedzi mówi sama za siebie. Podejście Dantilla odłącza stdin od aplikacji konsoli. Poniżej przedstawiam wersję C podejścia Kennedy'ego do „chwilowego migotania” jako osobną odpowiedź (tak, wiem, OP opublikował o C #). Użyłem go kilka razy i jestem z niego całkiem zadowolony.
willus
Można to zrobić w Javie ..)
Antoniossss
11

Zajrzyj na blog Raymonda na ten temat:

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

Jego pierwsze zdanie: „Nie możesz, ale możesz spróbować to udawać”.

jdigital
źródło
W rzeczywistości .Net bardzo łatwo to „udawać”, ale ta odpowiedź jest technicznie poprawna.
Joel Coehoorn
6

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

Po prostu sprawdź argumenty wiersza poleceń przed elementami WinForms Application..

Powinienem dodać, że w .NET jest NIEZBĘDNIE łatwo po prostu stworzyć konsolę i projekty GUI w tym samym rozwiązaniu, które współdzielą wszystkie swoje zestawy oprócz main. W takim przypadku możesz sprawić, aby wersja wiersza poleceń po prostu uruchomiła wersję GUI, jeśli jest uruchomiona bez parametrów. Otrzymasz migającą konsolę.

Cade Roux
źródło
Istnienie parametrów wiersza poleceń nie jest pewnym wskaźnikiem pożaru. Wiele aplikacji dla systemu Windows może przyjmować parametry wiersza poleceń
Neil N
3
Chodziło mi o to, że jeśli ich nie ma, uruchom wersję GUI. Jeśli chcesz, aby wersja GUI została uruchomiona z parametrami, prawdopodobnie możesz mieć do tego parametr.
Cade Roux
5

Jest łatwy sposób na robienie tego, co chcesz. Zawsze go używam podczas pisania aplikacji, które powinny mieć zarówno CLI, jak i GUI. Aby to zadziałało, musisz ustawić „OutputType” na „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
        });
      }
    }
  }
user1566352
źródło
1
Uwielbiam to i działa dobrze na mojej maszynie deweloperskiej z systemem Windows 7, jednak mam (wirtualną) maszynę z systemem Windows XP i wydaje się, że zrestartowany proces zawsze otrzymuje konsolę i znika w niekończącej się pętli restartując się. Jakieś pomysły?
Simon Hewitt,
1
Bądź bardzo ostrożny, w systemie Windows XP rzeczywiście prowadzi to do nieograniczonej pętli odradzania, którą bardzo trudno jest zabić.
użytkownik
3

Myślę, że preferowaną techniką jest to, co Rob nazwał techniką devenv , polegającą na użyciu dwóch plików wykonywalnych: programu uruchamiającego „.com” i oryginalnego „.exe”. Nie jest to trudne w użyciu, jeśli masz standardowy kod do pracy (patrz poniższy link).

Ta technika wykorzystuje sztuczki, aby ten „.com” był proxy dla stdin / stdout / stderr i uruchomił plik .exe o tej samej nazwie. Daje to zachowanie pozwalające programowi na wykonywanie wstępnych w trybie wiersza poleceń po wywołaniu z konsoli (potencjalnie tylko wtedy, gdy wykryte zostaną określone argumenty wiersza poleceń), podczas gdy nadal można uruchomić jako aplikację GUI wolną od konsoli.

Hostowałem projekt o nazwie dualsubsystem w Google Code, który aktualizuje stare rozwiązanie codeguru tej techniki i dostarcza kod źródłowy i działające przykładowe pliki binarne.

gabeiscoding
źródło
3

Oto, co uważam za proste rozwiązanie problemu w języku C # .NET. Aby powtórzyć problem, kiedy uruchamiasz konsolową „wersję” aplikacji z wiersza poleceń z przełącznikiem, konsola czeka (nie powraca do wiersza poleceń, a proces działa dalej), nawet jeśli masz Environment.Exit(0)na końcu twojego kodu. Aby to naprawić, tuż przed zadzwonieniem Environment.Exit(0), zadzwoń:

SendKeys.SendWait("{ENTER}");

Następnie konsola otrzymuje ostatni klawisz Enter, którego potrzebuje, aby powrócić do wiersza poleceń, i proces się kończy. Uwaga: nie dzwoń SendKeys.Send(), bo aplikacja ulegnie awarii.

Nadal trzeba dzwonić, AttachConsole()jak wspomniano w wielu postach, ale dzięki temu okno poleceń nie migocze podczas uruchamiania wersji aplikacji WinForm.

Oto cały kod w utworzonej przeze mnie przykładowej aplikacji (bez kodu 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());
            }
        }
    }
}

Mam nadzieję, że pomoże to komuś również spędzać dni nad tym problemem. Dzięki za podpowiedź przejdź do @dantill.

LTDev
źródło
Próbowałem tego i problem polega na tym, że wszystko, co zostało napisane przy użyciu Console.WriteLine, nie przesuwa kursora tekstowego konsoli (nadrzędnej). Dlatego po zamknięciu aplikacji kursor znajduje się w niewłaściwym miejscu i trzeba kilka razy nacisnąć klawisz Enter, aby powrócić do monitu o „wyczyszczenie”.
Tahir Hassan
@TahirHassan Możesz zautomatyzować szybkie przechwytywanie i czyszczenie zgodnie z opisem tutaj, ale nadal nie jest to idealne rozwiązanie: 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
źródło
2

Napisałem alternatywne podejście, które pozwala uniknąć flashowania konsoli. Zobacz, jak utworzyć program Windows, który działa zarówno jako graficzny interfejs użytkownika, jak i aplikacja konsoli .

dantill
źródło
1
Byłem sceptyczny, ale działa bez zarzutu. Naprawdę, naprawdę bez zarzutu. Świetna robota! Pierwsze prawdziwe rozwiązanie problemu, które widziałem. (Jest to kod C / C ++. Nie kod C #.)
B. Nadolson
Zgadzam się z B. Nadolsonem. Działa to (w C ++) bez ponownego uruchamiania procesu i bez wielu plików EXE.
GravityWell
2
Wady tej metody: (1) po zakończeniu musi wysłać dodatkowe naciśnięcie klawisza do konsoli, (2) nie może przekierować wyjścia konsoli do pliku i (3) najwyraźniej nie został przetestowany z dołączonym standardem Myślę, że nie można też przekierować z pliku). Dla mnie to zbyt wiele transakcji, aby uniknąć chwilowego flashowania okna konsoli. Metoda ponownego uruchomienia zapewnia przynajmniej prawdziwą podwójną konsolę / GUI. Rozprowadziłem taką aplikację do dziesiątek tysięcy użytkowników i nie otrzymałem ani jednej skargi ani komentarza na temat chwilowo migającego okna konsoli.
willus
0

Uruchom AllocConsole () w konstruktorze statycznym działa dla mnie

Kudłaty
źródło