Может ли один исполняемый файл быть одновременно консольным и графическим приложением?

81

Я хочу создать программу на C #, которую можно будет запускать как приложение CLI или GUI, в зависимости от того, какие флаги в него переданы. Это можно сделать?

Я нашел эти связанные вопросы, но они не совсем подходят для моей ситуации:

БКС
источник
1
Для справки: это действительно связано с операционной системой, а не с CLR. Например, с Mono в Linux нет проблем с созданием такого приложения (фактически, каждое приложение является консольным, но также может делать что угодно с Windows) - так же, как с Java или любой другой программой * nix. И общий шаблон - это вход в консоль при использовании графического интерфейса пользователя.
konrad.kruczynski

Ответы:

99

Ответ Jdigital указывает на блог Рэймонда Чена , в котором объясняется, почему у вас не может быть приложения, которое одновременно является консольной и неконсольной *программой: ОС должна знать перед запуском программы, какую подсистему использовать. После того, как программа запустилась, уже слишком поздно возвращаться и запрашивать другой режим.

Ответ Кейда указывает на статью о запуске приложения .Net WinForms с помощью консоли . Он использует технику вызова AttachConsoleпосле запуска программы. Это позволяет программе выполнять обратную запись в окно консоли командной строки, запустившей программу. Но комментарии в этой статье указывают на то, что я считаю фатальной ошибкой: дочерний процесс на самом деле не управляет консолью. Консоль продолжает принимать ввод от имени родительского процесса, и родительский процесс не знает, что ему следует дождаться завершения дочернего процесса, прежде чем использовать консоль для других целей.

В статье Чена есть ссылка на статью Цзюньфэна Чжана, в которой объясняются еще несколько техник .

Первое - это то, что использует devenv . Он работает, фактически имея две программы. Один из них - devenv.exe , основная программа с графическим интерфейсом, а другой - devenv.com , который обрабатывает задачи в консольном режиме, но если он используется не консольным образом, он перенаправляет свои задачи на devenv.exe и выходы. Этот метод основан на правиле Win32, согласно которому com- файлы выбираются перед exe- файлами, когда вы вводите команду без расширения файла.

Есть более простой вариант, который делает Windows Script Host. Он предоставляет две совершенно отдельные исполняемые файлы, wscript.exe и cscript.exe . Точно так же Java предоставляет java.exe для консольных программ и javaw.exe для неконсольных программ.

Илдазм использует вторую технику Джунфэна . Он цитирует процесс, через который прошел автор ildasm, заставляя его работать в обоих режимах. В конечном итоге вот что он делает:

  1. Программа помечена как двоичный файл консольного режима, поэтому всегда запускается с консоли. Это позволяет перенаправлению ввода и вывода работать в обычном режиме.
  2. Если программа не имеет параметров командной строки в режиме консоли, она перезапускается.

Недостаточно просто вызвать, FreeConsoleчтобы первый экземпляр перестал быть консольной программой. Это связано с тем, что процесс, запустивший программу, cmd.exe , «знает», что он запустил программу в режиме консоли, и ожидает, когда программа остановится. Вызов FreeConsoleзаставит ildasm перестать использовать консоль, но не заставит родительский процесс начать использовать консоль.

Итак, первый экземпляр перезагружается (полагаю, с дополнительным параметром командной строки). Когда вы вызываете CreateProcess, есть два разных флага, которые нужно попробовать, DETACHED_PROCESSиCREATE_NEW_CONSOLE любой из них гарантирует, что второй экземпляр не будет прикреплен к родительской консоли. После этого первый экземпляр может завершить работу и разрешить командной строке возобновить обработку команд.

Побочный эффект этого метода заключается в том, что когда вы запускаете программу из графического интерфейса пользователя, консоль все равно будет. Он на мгновение мигнет на экране, а затем исчезнет.

Я думаю, что часть статьи Джунфэна об использовании editbin для изменения флага консольного режима программы - отвлекающий маневр. Ваш компилятор или среда разработки должны предоставлять параметр или параметр для управления типом создаваемого двоичного файла. После этого не нужно ничего изменять.

Суть в том, что у вас может быть либо два двоичных файла, либо у вас может быть мгновенное мерцание окна консоли . Как только вы решите, какое из зол меньшее, у вас будет выбор вариантов реализации.

*Я говорю « неконсольный» вместо GUI, потому что в противном случае это ложная дихотомия. То, что у программы нет консоли, не означает, что у нее есть графический интерфейс. Приложение-служба - яркий тому пример. Также программа может иметь консоль и окна.

Роб Кеннеди
источник
Я знаю, что это старый ответ, но что касается отвлекающих факторов, касающихся editbin, я считаю, что цель этого трюка - заставить CRT связать WinMainфункцию с соответствующими параметрами (так что скомпилировать с /SUBSYSTEM:WINDOWS), а затем изменить режим постфактум, чтобы загрузчик запускает консольный хост. Для получения дополнительной обратной связи я пробовал это CREATE_NO_WINDOWв CreateProcess и в GetConsoleWindow() == NULLкачестве проверки, был ли перезапущен или нет. Это не устраняет мерцание консоли, но означает отсутствие специального аргумента cmd.
Это отличный ответ, но для полноты картины, вероятно, стоит указать, в чем заключаются основные различия между консольной и «неконсольной» программой (недоразумение здесь, кажется, приводит ко многим ошибочным ответам ниже). То есть консольное приложение, запущенное из консоли, не вернет управление родительской консоли до тех пор, пока оно не будет завершено, тогда как приложение с графическим интерфейсом будет разветвлено и немедленно вернется. Если вы не уверены, вы можете использовать DUMPBIN / headers и искать строку SUBSYSTEM, чтобы точно увидеть, какой у вас вкус.
piers7
Это устаревший лучший ответ. По крайней мере, с точки зрения C / C ++. См. Нижеприведенное решение dantill для Win32, которое, вероятно, может быть кем-то адаптировано для C #.
Б. Надолсон
1
Я не считаю этот ответ устаревшим. Метод работает хорошо, и рейтинг ответа говорит сам за себя. Подход Дантилла отключает стандартный ввод от консольного приложения. Я предоставил C-версию подхода Кеннеди к "мгновенному мерцанию" ниже в качестве отдельного ответа (да, я знаю, OP опубликовал о C #). Я использовал его несколько раз и вполне им доволен.
willus
Вы можете сделать это на Java ..)
Antoniossss
11

Посмотрите блог Раймонда по этой теме:

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

Его первое предложение: «Вы не можете, но вы можете попытаться подделать это».

jdigital
источник
.Net на самом деле позволяет довольно легко "подделать", но этот ответ технически правильный.
Joel Coehoorn
6

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

Просто проверьте аргументы командной строки перед Application.материалом WinForms .

Я должен добавить, что в .NET НЕДОСТАТОЧНО легко просто создать консоль и проекты GUI в одном решении, которые разделяют все свои сборки, кроме main. И в этом случае вы можете заставить версию командной строки просто запускать версию с графическим интерфейсом, если она запускается без параметров. Вы получите мигающую консоль.

Кейд Ру
источник
Наличие параметров командной строки вряд ли является верным признаком пожара. Множество приложений Windows могут принимать параметры командной строки
Нил Н.
3
Я хотел сказать, что если их нет, запустите версию с графическим интерфейсом. Если вы хотите, чтобы версия GUI запускалась с параметрами, по-видимому, у вас может быть параметр для этого.
Cade Roux
5

Есть простой способ делать то, что вы хотите. Я всегда использую его при написании приложений, которые должны иметь как интерфейс командной строки, так и графический интерфейс. Вы должны установить свой "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
        });
      }
    }
  }
user1566352
источник
1
Мне это нравится, и он отлично работает на моей машине с Windows 7. Однако у меня есть (виртуальная) машина с Windows XP, и кажется, что перезапущенный процесс всегда получает консоль и поэтому исчезает в бесконечном цикле, перезагружаясь. Есть идеи?
Саймон Хьюитт
1
Будьте очень осторожны с этим, в Windows XP это действительно приводит к неограниченному циклу возрождения, который очень трудно убить.
пользователь
3

Я думаю, что предпочтительнее то, что Роб назвал devenv , с использованием двух исполняемых файлов: программы запуска «.com» и оригинального «.exe». Это не так сложно использовать, если у вас есть шаблонный код для работы (см. Ссылку ниже).

В этом методе используются уловки, позволяющие сделать этот .com прокси-сервером для stdin / stdout / stderr и запустить одноименный файл .exe. Это дает возможность программе преформироваться в режиме командной строки при вызове из консоли (возможно, только при обнаружении определенных аргументов командной строки), в то же время имея возможность запускаться как приложение с графическим интерфейсом без консоли.

Я разместил в 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.

LTDev
источник
Я пробовал это, и проблема в том, что все, что написано с использованием Console.WriteLine, не перемещает текстовый курсор (родительской) консоли. Поэтому при выходе из приложения курсор находится не в том месте, и вам нужно несколько раз нажать клавишу ввода, чтобы вернуть его к «чистой» подсказке.
Тахир Хассан
@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);
    }
Willus
источник
1

Я написал альтернативный подход, который позволяет избежать вспышки консоли. См. Раздел Как создать программу для Windows, которая работает как в графическом интерфейсе, так и в консольном приложении .

Дантилль
источник
1
Я был настроен скептически, но это работает безупречно. Вроде действительно, действительно безупречно. Отличная работа! Первое верное решение проблемы, которое я видел. (Это код C / C ++. Не код C #.)
Б. Надолсон,
Я согласен с Б. Надольсоном. Это работает (для C ++) без перезапуска процесса и без нескольких EXE.
GravityWell
2
Недостатки этого метода: (1) он должен отправить дополнительное нажатие клавиши на консоль, когда это будет сделано, (2) он не может перенаправить вывод консоли в файл и (3) он, по-видимому, не был протестирован с подключенным stdin (который Думаю, тоже нельзя перенаправить из файла). На мой взгляд, это слишком много сделок, чтобы на мгновение не вспыхнуть окно консоли. Метод повторного запуска, по крайней мере, обеспечивает настоящую двойную консоль / графический интерфейс. Я распространил такое приложение среди десятков тысяч пользователей и не получил ни одной жалобы или комментария по поводу мгновенно мигающего окна консоли.
Willus 02
0

Запустить AllocConsole () в статическом конструкторе у меня работает

Лохматый
источник