Чи може один виконуваний файл бути як консоллю, так і програмою графічного інтерфейсу?


81

Я хочу створити програму на C #, яку можна запускати як програму CLI або графічний інтерфейс, залежно від того, які прапорці в неї передаються. Чи можна це зробити?

Я знайшов ці відповідні запитання, але вони точно не охоплюють мою ситуацію:



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. Він надає два повністю окремі двійкові файли, wscript.exe та cscript.exe . Подібним чином Java надає java.exe для консольних програм та javaw.exe для програм.

Другий прийом Юнфенга - це те, що використовує ільдазм . Він цитує процес, який пройшов автор ildasm , працюючи в обох режимах. Зрештою, ось що це робить:

  1. Програма позначена як двійковий файл у консольному режимі, тому вона завжди починається з консолі. Це дозволяє переспрямуванню введення та виводу працювати нормально.
  2. Якщо програма не має параметрів командного рядка в консольному режимі, вона повторно запускається сама.

Недостатньо просто зателефонувати, FreeConsoleщоб перший екземпляр перестав бути консольною програмою. Це тому, що процес, який запустив програму, cmd.exe , "знає", що він запустив програму в консольному режимі і чекає, поки програма перестане працювати. Виклик FreeConsoleпризведе до того, що ildasm припинить використання консолі, але батьківський процес не почне використовувати консоль.

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

Побічним ефектом цієї техніки є те, що при запуску програми з інтерфейсу графічного інтерфейсу все одно буде консоль. На мить він блиматиме на екрані, а потім зникне.

Частина статті Юнфенга про використання editbin для зміни прапора в консольному режимі програми - це, на мою думку, червоний оселедець. Ваш компілятор або середовище розробки повинні забезпечити параметр або опцію для контролю того, який тип двійкового файлу він створює. Після цього не повинно бути необхідності щось змінювати.

Суть в тому, що у вас може бути два двійкові файли, або ви можете миттєво мерехтіти у вікні консолі . Коли ви вирішите, що є меншим злом, у вас є вибір варіантів реалізації.

*Я кажу неконсольний замість графічного інтерфейсу, оскільки в іншому випадку це помилкова дихотомія. Те, що програма не має консолі, не означає, що вона має графічний інтерфейс. Службовий додаток є яскравим прикладом. Також програма може мати консоль і вікна.


Я знаю, що це стара відповідь, але щодо пунктів червоного оселедця щодо editbin, я вважаю, що мета цього фокусу - змусити ЕЛТ зв’язати WinMainфункцію з відповідними параметрами (так скомпілювати /SUBSYSTEM:WINDOWS), а потім змінити режим постфактум так завантажувач запускає консольний хост. Щоб отримати додаткові відгуки, я спробував це CREATE_NO_WINDOWв CreateProcess і GetConsoleWindow() == NULLяк свою перевірку, перезапустив чи ні. Це не виправляє мерехтіння консолі, але це означає відсутність спеціального аргументу cmd.

Це чудова відповідь, але для повноти, мабуть, варто вказати, в чому полягають основні відмінності між консоллю та неконсольною програмою (непорозуміння тут, здається, призводить до багатьох помилкових відповідей нижче). Тобто: програма консолі, запущена з консолі, не поверне керування батьківській консолі, поки не буде завершена, тоді як програма графічного інтерфейсу розгалужується і негайно повернеться. Якщо ви не впевнені, ви можете використовувати DUMPBIN / заголовки та шукати рядок SUBSYSTEM, щоб точно побачити, який смак у вас є.
пристані7,

Це застаріла найкраща відповідь. Принаймні з точки зору C / C ++. Дивіться рішення dantill нижче для Win32, яке, можливо, хтось може адаптувати до C #.
Б. Надолсон

1
Я не вважаю цю відповідь застарілою. Метод працює добре, і рейтинг відповіді говорить сам за себе. Підхід Dantill відключає stdin від консольного додатка. Я надав нижченаведену C-версію підходу Кеннеді щодо "миттєвого мерехтіння" нижче як окрему відповідь (так, я знаю, ОП розмістив про C #). Я використовував його кілька разів і цілком задоволений.
willus

Ви можете зробити це на Java ..)
Antoniossss,


6

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

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

Слід додати, що в .NET СПИСОЧНО просто зробити консоль та проекти графічного інтерфейсу в одному і тому ж рішенні, які мають спільний доступ до всіх своїх збірок, крім основного. І в цьому випадку ви можете змусити версію командного рядка просто запустити версію графічного інтерфейсу, якщо вона запускається без параметрів. Ви отримаєте миготливу консоль.


Існування параметрів командного рядка навряд чи є надійним показником пожежі. Багато програм для Windows можуть приймати параметри командного рядка
Ніл Н,

3
Я мав на меті, якщо таких немає, запустити графічну версію. Якщо ви хочете, щоб версія графічного інтерфейсу запускалася з параметрами, можливо, у вас може бути параметр для цього.
Кейд Ру

5

Існує простий спосіб зробити те, що ти хочеш. Я завжди використовую його, коли пишу програми, які повинні мати як CLI, так і GUI. Вам потрібно встановити для параметра "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
        });
      }
    }
  }

1
Мені це подобається, і він чудово працює на моїй розробницькій машині Windows 7. Однак у мене є (віртуальна) машина Windows XP, і здається, що перезапущений процес завжди отримує консоль і тому зникає в нескінченному циклі, перезавантажуючись сам. Будь-які ідеї?
Саймон Хьюїтт,

1
Будьте дуже обережні з цим, у Windows XP це справді призводить до необмеженого циклу відродження, який дуже важко вбити.
користувач

3

Я вважаю, що кращою технікою є те, що Роб назвав технікою devenv використання двох виконуваних файлів: панелі запуску ".com" та оригінальної ".exe". Це не так складно використовувати, якщо у вас є шаблонний код для роботи (див. Посилання нижче).

Ця техніка використовує фокуси, щоб ".com" був проксі-сервером stdin / stdout / stderr і запускав однойменний файл .exe. Це дає поведінку дозволу програмі здійснювати попередню підготовку в режимі командного рядка при виклику з форми консолі (потенційно лише тоді, коли виявляються певні аргументи командного рядка), але при цьому все ще може запускатися як програма графічного інтерфейсу, вільна від консолі.

Я розмістив проект під назвою dualsubsystem в Google Code, який оновлює старе рішення коду-гуру цієї техніки та надає вихідний код та робочі прикладні двійкові файли.


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.


Я спробував це, і проблема полягає в тому, що все, що написано за допомогою Console.WriteLine, не висуває текстовий курсор (батьківської) консолі. Отже, коли ви виходите з програми, позиція курсора знаходиться в неправильному місці, і вам потрібно натиснути клавішу Enter кілька разів, щоб повернути її до "чистого" запиту.
Тахір Хасан

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

1

Я написав альтернативний підхід, який дозволяє уникнути спалаху консолі. Див. Розділ Як створити програму Windows, яка працює як графічний інтерфейс, так і як консольний додаток .


1
Я був скептично налаштований, але це працює бездоганно. Як справді, дуже бездоганно. Відмінна робота! Перше справжнє рішення проблеми, яке я бачив. (Це код C / C ++. Не код C #.)
Б. Надолсон

Я згоден з Б. Надольсоном. Це працює (для C ++), без перезапуску процесу та без декількох EXE.
GravityWell

2
Недоліки цього методу: (1) він повинен надіслати додатковий натискання клавіші на консоль, коли це буде зроблено, (2) він не може перенаправити висновок консолі у файл, і (3) він, мабуть, не був перевірений із вкладеним stdin (який Я думаю, також не можна перенаправляти з файлу). Для мене це занадто багато торгів, щоб уникнути миттєвого миготіння вікна консолі. Метод повторного запуску принаймні забезпечує справжню подвійну консоль / графічний інтерфейс. Я розповсюдив такий додаток серед десятків тисяч користувачів і не отримав жодної скарги чи коментаря щодо миттєво блимаючого вікна консолі.
willus

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.