Một tệp thực thi có thể vừa là bảng điều khiển vừa là ứng dụng GUI không?


81

Tôi muốn tạo một chương trình C # có thể chạy dưới dạng ứng dụng CLI hoặc GUI tùy thuộc vào những cờ nào được chuyển vào nó. Điều này có thể được thực hiện?

Tôi đã tìm thấy những câu hỏi liên quan này, nhưng chúng không bao gồm chính xác tình huống của tôi:



1
Chỉ đối với hồ sơ: nó thực sự liên quan đến hệ điều hành, không phải CLR. Ví dụ: với Mono trên Linux, không có vấn đề gì khi tạo ứng dụng như vậy (trên thực tế, mọi ứng dụng đều là bảng điều khiển, nhưng cũng có thể làm bất cứ điều gì với windows) - giống như với Java hoặc bất kỳ chương trình * nix nào khác. Và mô hình chung là đăng nhập trên bảng điều khiển trong khi sử dụng GUI cho người dùng.
konrad.kruczynski

Câu trả lời:


99

Câu trả lời của Jdigital chỉ vào blog của Raymond Chen , điều này giải thích tại sao bạn không thể có một ứng dụng vừa là chương trình console vừa là chương trình không phải console *: Hệ điều hành cần biết trước khi chương trình bắt đầu chạy hệ thống con nào để sử dụng. Khi chương trình đã bắt đầu chạy, đã quá muộn để quay lại và yêu cầu chế độ khác.

Câu trả lời của Cade hướng đến một bài báo về cách chạy ứng dụng .Net WinForms với bảng điều khiển . Nó sử dụng kỹ thuật gọi AttachConsolesau khi chương trình bắt đầu chạy. Điều này có tác dụng cho phép chương trình ghi trở lại cửa sổ giao diện điều khiển của dấu nhắc lệnh đã khởi động chương trình. Nhưng các bình luận trong bài báo đó chỉ ra điều mà tôi coi là một lỗ hổng chết người: Quá trình con không thực sự kiểm soát bàn điều khiển. Bảng điều khiển tiếp tục chấp nhận đầu vào thay mặt cho quy trình mẹ và quy trình mẹ không biết rằng nó nên đợi phần tử con chạy xong trước khi sử dụng bảng điều khiển cho những việc khác.

Bài báo của Chen chỉ ra một bài báo của Junfeng Zhang giải thích một số kỹ thuật khác .

Đầu tiên là những gì devenv sử dụng. Nó hoạt động bằng cách thực sự có hai chương trình. Một là devenv.exe , là chương trình GUI chính và chương trình còn lại là devenv.com , xử lý các tác vụ ở chế độ bảng điều khiển, nhưng nếu nó được sử dụng theo cách không giống như bảng điều khiển, nó sẽ chuyển tiếp các tác vụ của mình tới devenv.exe và lối thoát hiểm. Kỹ thuật này dựa trên quy tắc Win32 mà tệp com được chọn trước tệp exe khi bạn nhập lệnh không có phần mở rộng tệp.

Có một biến thể đơn giản hơn về điều này mà Windows Script Host thực hiện. Nó cung cấp hai tệp nhị phân hoàn toàn riêng biệt, wscript.execscript.exe . Tương tự như vậy, Java cung cấp java.exe cho các chương trình console và javaw.exe cho các chương trình không phải console.

Kỹ thuật thứ hai của Junfeng là những gì ildasm sử dụng. Anh ấy trích dẫn quá trình mà tác giả của ildasm đã trải qua khi làm cho nó chạy ở cả hai chế độ. Cuối cùng, đây là những gì nó làm:

  1. Chương trình được đánh dấu là một nhị phân chế độ giao diện điều khiển, vì vậy nó luôn bắt đầu với một bàn điều khiển. Điều này cho phép chuyển hướng đầu vào và đầu ra hoạt động như bình thường.
  2. Nếu chương trình không có tham số dòng lệnh ở chế độ console, chương trình sẽ tự khởi chạy lại.

Nó không đủ để chỉ gọi FreeConsoleđể làm cho phiên bản đầu tiên không còn là một chương trình console. Đó là bởi vì quá trình khởi động chương trình, cmd.exe , "biết" rằng nó đã khởi động một chương trình ở chế độ bảng điều khiển và đang đợi chương trình ngừng chạy. Việc gọi FreeConsolesẽ khiến ildasm ngừng sử dụng bảng điều khiển, nhưng nó sẽ không làm cho quy trình mẹ bắt đầu sử dụng bảng điều khiển.

Vì vậy, phiên bản đầu tiên tự khởi động lại (với một tham số dòng lệnh bổ sung, tôi cho là vậy). Khi bạn gọi CreateProcess, có hai cờ khác nhau để thử DETACHED_PROCESSCREATE_NEW_CONSOLE , một trong hai cờ sẽ đảm bảo rằng phiên bản thứ hai sẽ không được gắn vào bảng điều khiển mẹ. Sau đó, phiên bản đầu tiên có thể kết thúc và cho phép dấu nhắc lệnh tiếp tục các lệnh xử lý.

Tác dụng phụ của kỹ thuật này là khi bạn khởi động chương trình từ giao diện GUI, vẫn sẽ có một giao diện điều khiển. Nó sẽ nhấp nháy trên màn hình trong giây lát và sau đó biến mất.

Tôi nghĩ rằng phần trong bài viết của Junfeng về việc sử dụng editbin để thay đổi cờ chế độ bảng điều khiển của chương trình là một con cá trích đỏ. Trình biên dịch hoặc môi trường phát triển của bạn nên cung cấp một cài đặt hoặc tùy chọn để kiểm soát loại nhị phân mà nó tạo ra. Không cần phải sửa đổi bất cứ điều gì sau đó.

Điểm mấu chốt là bạn có thể có hai tệp nhị phân hoặc bạn có thể nhấp nháy giây lát cửa sổ bảng điều khiển . Một khi bạn quyết định cái nào ít ác hơn, bạn có quyền lựa chọn triển khai của mình.

*Tôi nói non-console thay vì GUI bởi vì nếu không thì đó là một sự phân đôi sai. Chỉ vì một chương trình không có bảng điều khiển không có nghĩa là nó có GUI. Một ứng dụng dịch vụ là một ví dụ điển hình. Ngoài ra, một chương trình có thể có bảng điều khiển cửa sổ.


Tôi biết đây là một câu trả lời cũ, nhưng trên các điểm cá trích về editbin, tôi tin rằng mục đích của thủ thuật đó là để CRT liên kết một WinMainhàm với các tham số thích hợp (nên biên dịch với /SUBSYSTEM:WINDOWS) sau đó thay đổi chế độ. trình tải khởi chạy máy chủ bảng điều khiển. Để có thêm phản hồi, tôi đã thử điều này CREATE_NO_WINDOWtrong CreateProcess và GetConsoleWindow() == NULLkhi kiểm tra xem có khởi chạy lại hay không. Điều này không khắc phục được sự nhấp nháy của bảng điều khiển, nhưng nó không có nghĩa là không có đối số cmd đặc biệt.

Đây là một câu trả lời tuyệt vời, nhưng để đầy đủ, có lẽ cần nêu rõ sự khác biệt chính giữa chương trình console và chương trình 'non-console' là gì (sự hiểu lầm ở đây dường như dẫn đến nhiều câu trả lời nhầm lẫn bên dưới). Đó là: một ứng dụng console, được khởi chạy từ console, sẽ không trả lại quyền kiểm soát cho bảng điều khiển mẹ cho đến khi nó hoàn thành, trong khi ứng dụng GUI sẽ phân nhánh và trở lại ngay lập tức. Khi không chắc chắn, bạn có thể sử dụng DUMPBIN / headers và tìm dòng SUBSYSTEM để xem chính xác hương vị bạn có.
piers7

Đây là một câu trả lời hay nhất đã lỗi thời. Ít nhất là từ góc độ C / C ++. Xem giải pháp của dantill bên dưới dành cho Win32, giải pháp này có thể được ai đó thích nghi với C #.
B. Nadolson

1
Tôi không coi câu trả lời này là lỗi thời. Phương pháp này hoạt động tốt và đánh giá của câu trả lời tự nó nói lên điều đó. Cách tiếp cận của Dantill ngắt kết nối stdin khỏi ứng dụng bảng điều khiển. Tôi đã cung cấp phiên bản C của phương pháp tiếp cận "nhấp nháy tạm thời" của Kennedy bên dưới như một câu trả lời riêng (vâng, tôi biết, OP đã đăng về C #). Tôi đã sử dụng nó vài lần và khá hài lòng với nó.
willus

Bạn có thể làm điều này trong Java ..)
Antoniossss


6

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

Chỉ cần kiểm tra các đối số dòng lệnh trước Application.công cụ WinForms .

Tôi nên nói thêm rằng trong .NET, thật dễ dàng RIDICULOUSLY để tạo một bảng điều khiển và các dự án GUI trong cùng một giải pháp chia sẻ tất cả các assembly của chúng ngoại trừ main. Và trong trường hợp này, bạn có thể tạo phiên bản dòng lệnh chỉ cần khởi chạy phiên bản GUI nếu nó được khởi chạy không có tham số. Bạn sẽ nhận được một bảng điều khiển nhấp nháy.


Sự tồn tại của các tham số dòng lệnh hầu như không phải là một dấu hiệu cháy chắc chắn. Rất nhiều cửa sổ ứng dụng có thể lấy params dòng lệnh
Neil N

3
Quan điểm của tôi là nếu không có, hãy khởi chạy phiên bản GUI. Nếu bạn muốn phiên bản GUI được khởi chạy với các tham số, có lẽ bạn có thể có một tham số cho điều đó.
Cade Roux

5

Có một cách dễ dàng để làm những gì bạn muốn. Tôi luôn sử dụng nó khi viết các ứng dụng phải có cả CLI và GUI. Bạn phải đặt "OutputType" của mình thành "ConsoleApplication" để điều này hoạt động.

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
Tôi thích điều này và nó hoạt động tốt trên máy phát triển Windows 7 của tôi.Tuy nhiên, tôi có một máy Windows XP (ảo) và có vẻ như quá trình khởi động lại luôn nhận được một bảng điều khiển và do đó, nó biến mất trong một vòng lặp vô tận tự khởi động lại. Có ý kiến ​​gì không?
Simon Hewitt

1
Hãy cẩn thận với điều này, trên Windows XP, điều này thực sự dẫn đến một vòng lặp hồi sinh không giới hạn và rất khó để giết.
người dùng

3

Tôi nghĩ rằng kỹ thuật ưa thích là kỹ thuật mà Rob gọi là kỹ thuật devenv sử dụng hai tệp thực thi: trình khởi chạy ".com" và ".exe" ban đầu. Đây không phải là khó sử dụng nếu bạn có mã soạn sẵn để làm việc (xem liên kết bên dưới).

Kỹ thuật này sử dụng các thủ thuật để có ".com" làm proxy cho stdin / stdout / stderr và khởi chạy tệp .exe có cùng tên. Điều này cung cấp hành vi cho phép chương trình định dạng sẵn trong chế độ dòng lệnh khi được gọi là bảng điều khiển (chỉ có khả năng khi một số đối số dòng lệnh nhất định được phát hiện) trong khi vẫn có thể khởi chạy dưới dạng ứng dụng GUI không có bảng điều khiển.

Tôi đã tổ chức một dự án có tên là hệ thống dualsubsystem trên Google Code cập nhật một giải pháp codeguru cũ của kỹ thuật này và cung cấp mã nguồn và mã nhị phân mẫu hoạt động.


3

Đây là những gì tôi tin là giải pháp .NET C # đơn giản cho vấn đề. Chỉ để khắc phục sự cố, khi bạn chạy bảng điều khiển "phiên bản" của ứng dụng từ một dòng lệnh có nút chuyển, bảng điều khiển tiếp tục chờ (nó không quay trở lại dấu nhắc lệnh và quá trình tiếp tục chạy) ngay cả khi bạn có Environment.Exit(0)ở cuối mã của bạn. Để khắc phục điều này, ngay trước khi gọi Environment.Exit(0), hãy gọi số này:

SendKeys.SendWait("{ENTER}");

Sau đó, bảng điều khiển nhận được phím Enter cuối cùng mà nó cần để quay lại dấu nhắc lệnh và quá trình kết thúc. Lưu ý: Đừng gọi SendKeys.Send(), nếu không ứng dụng sẽ bị lỗi.

Vẫn cần thiết để gọi AttachConsole()như đã đề cập trong nhiều bài viết, nhưng với điều này, tôi không thấy cửa sổ lệnh nhấp nháy khi khởi chạy phiên bản WinForm của ứng dụng.

Đây là toàn bộ mã trong một ứng dụng mẫu mà tôi đã tạo (không có mã 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());
            }
        }
    }
}

Hy vọng nó sẽ giúp một ai đó cũng dành nhiều ngày cho vấn đề này. Cảm ơn bạn đã gợi ý cho @dantill.


Tôi đã thử điều này và vấn đề là mọi thứ được viết bằng cách sử dụng Console.WriteLinekhông nâng cao con trỏ văn bản của bảng điều khiển (cha). Vì vậy, khi ứng dụng của bạn thoát ra, vị trí con trỏ ở sai vị trí và bạn phải nhấn enter một vài lần chỉ để đưa nó trở lại lời nhắc "sạch".
Tahir Hassan

@TahirHassan Bạn có thể tự động hóa việc thu thập và dọn dẹp nhanh chóng như được mô tả ở đây, nhưng nó vẫn không phải là giải pháp hoàn hảo: 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

Tôi đã viết ra một cách tiếp cận thay thế tránh đèn flash bảng điều khiển. Xem Cách tạo chương trình Windows hoạt động cả dưới dạng GUI và ứng dụng bảng điều khiển .


1
Tôi đã nghi ngờ nhưng nó hoạt động hoàn hảo. Giống như thực sự, thực sự hoàn hảo. Công việc tuyệt vời! Giải pháp thực sự đầu tiên cho vấn đề mà tôi đã thấy. (Đây là mã C / C ++. Không phải mã C #.)
B. Nadolson

Tôi đồng ý với B. Nadolson. Điều này hoạt động (đối với C ++), mà không cần khởi chạy lại quy trình và không có nhiều EXE.
GravityWell,

2
Hạn chế của phương pháp này: (1) nó phải gửi thêm một lần nhấn phím đến bảng điều khiển khi hoàn tất, (2) nó không thể chuyển hướng đầu ra bảng điều khiển thành một tệp và (3) nó dường như chưa được kiểm tra với stdin đính kèm (mà Tôi đoán cũng không thể được chuyển hướng từ một tệp). Đối với tôi, đó là quá nhiều giao dịch chỉ để tránh nhấp nháy cửa sổ giao diện điều khiển trong giây lát. Phương pháp khởi chạy lại ít nhất cũng cung cấp giao diện điều khiển / GUI kép thực sự. Tôi đã phân phối một ứng dụng như vậy cho hàng chục nghìn người dùng và chưa nhận được một lời phàn nàn hay nhận xét nào về cửa sổ bảng điều khiển nhấp nháy trong giây lát.
willus

0

Chạy AllocConsole () trong một hàm tạo tĩnh hoạt động đối với tôi

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.