現代應用程式常需支援雙模式:GUI 模式 (雙擊執行、開始功能表) 與 命令列模式 (排程器、CI/CD、自動化腳本呼叫)。核心挑戰在於如何可靠偵測目前執行環境。本文提供三種策略,並示範「單一 EXE、雙模式、共用邏輯」的完整架構。
| 策略 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 1. 參數約定 | 以第一個引數決定模式 (如 /console、/gui) | 明確、可控、跨平台、無相依 | 需呼叫端配合 | 新專案、自動化腳本、CI/CD |
| 2. 父進程偵測 | 檢查父進程名稱是否為 cmd.exe、powershell.exe、wt.exe | 無需修改呼叫方式、自動偵測 | 若從 Run 對話框、捷徑啟動父進程為 explorer.exe,會誤判為 GUI | 現有專案改造、互動式 CLI 使用 |
| 3. 主控台附掛測試 | 嘗試 AttachConsole(ATTACH_PARENT_PROCESS),成功表示有主控台 | 最精確判斷「是否連接主控台」 | 若從已有主控台的 GUI App (如 VS Output 視窗) 啟動會誤判 | 需精確控制主控台輸出行為 |
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public static class Program
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
private const int ATTACH_PARENT_PROCESS = -1;
[STAThread]
public static void Main(string[] args)
{
// 1. 解析模式參數 (最高優先級)
string mode = "gui"; // 預設 GUI
if (args.Length > 0 && !args[0].StartsWith("/") && !args[0].StartsWith("-"))
{
// 非參數格式,視為檔案路徑等業務參數,維持 GUI
}
else if (args.Length > 0)
{
mode = args[0].TrimStart('/', '-').ToLowerInvariant();
}
// 2. 依模式分流
switch (mode)
{
case "console":
case "cli":
RunConsoleMode(args);
break;
case "gui":
default:
// 3. 自動偵測:若從 cmd/powershell 啟動且未指定 /gui,可選擇自動進入 CLI
if (IsLaunchedFromConsole() && args.Length == 0)
{
Console.WriteLine("偵測到從命令列啟動,自動進入 CLI 模式 (加 /gui 強制 GUI)");
RunConsoleMode(args);
}
else
{
RunGuiMode();
}
break;
}
}
private static bool IsLaunchedFromConsole()
{
try
{
IntPtr fg = GetForegroundWindow();
GetWindowThreadProcessId(fg, out int pid);
var parent = Process.GetProcessById(pid);
string name = parent.ProcessName.ToLowerInvariant();
return name == "cmd" || name == "powershell" || name == "wt" || name == "terminal";
}
catch { return false; }
}
private static void RunConsoleMode(string[] args)
{
// 嘗試附掛父主控台 (若從 cmd 啟動)
bool attached = AttachConsole(ATTACH_PARENT_PROCESS);
if (!attached)
{
// 無父主控台 (如雙擊、排程器) → 配置新主控台
AllocConsole();
}
try
{
Console.WriteLine($"[CLI Mode] 啟動參數: {string.Join(" ", args)}");
// 執行 CLI 邏輯
var cli = new CliEngine();
int exitCode = cli.Execute(args);
Environment.ExitCode = exitCode;
}
finally
{
if (attached) FreeConsole(); // 釋放父主控台
else { /* 新配置的主控台隨進程結束自動關閉 */ }
}
}
private static void RunGuiMode()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm()); // WPF 用 new App().Run(new MainWindow())
}
}
// --- CLI 邏輯封裝 ---
public class CliEngine
{
public int Execute(string[] args)
{
if (args.Length == 0 || args[0] == "/?" || args[0] == "/help")
{
ShowHelp();
return 0;
}
// 實際業務邏輯
Console.WriteLine("執行 CLI 工作...");
return 0;
}
private void ShowHelp()
{
Console.WriteLine(@"
MyApp - 雙模式應用程式
用法: MyApp.exe [/gui | /console] [參數...]
/gui 強制啟動 GUI 模式
/console 強制啟動命令列模式
/? 顯示說明
");
}
}
// App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
// 同樣解析 e.Args 決定模式
if (ShouldRunCli(e.Args))
{
RunCliMode(e.Args);
Shutdown(); // 結束 WPF 應用程式
}
else
{
base.OnStartup(e); // 正常啟動 GUI
}
}
}
專案屬性 → Debug → Application arguments:
/gui/console arg1 arg2若需在 GUI 模式下也能看到 Console.WriteLine (如 Visual Studio Output 視窗):
[DllImport("kernel32.dll")]
private static extern bool AttachConsole(int dwProcessId);
public static void EnsureConsoleAttached()
{
// 嘗試附掛父進程主控台 (VS 偵錯時有效)
AttachConsole(ATTACH_PARENT_PROCESS);
}
/console 參數。AttachConsole 失敗且未 AllocConsole,標準輸入無效。確保 RunConsoleMode 邏輯正確。/gui、/console),最清晰、可測試、跨平台。AttachConsole/AllocConsole/FreeConsole 組合,精確控制主控台生命週期。