Hmmmmm…… Co to jest hybrydowa aplikacja WPF? Próbowałem znaleźć jakieś inne określenie na ten problem, ale niestety to wydaje się najtrafniejsze. Pojęciem aplikacja hybrydowa WPF będę nazywał aplikację, które działa i prezentuje wyniki działania w konsoli, jak również we własnym oknie. Dodatkowo aplikacja powinna wspierać przekazywanie parametrów w trakcie startu.

Analizując przedstawione wymagania możemy spodziewać się następujących scenariuszy:

  • chcemy uruchomić aplikację w trybie graficznym bez podawania parametrów,
  • chcemy uruchomić aplikację w trybie graficznym z dodatkowymi parametrami,
  • chcemy uruchomić aplikację w trybie konsoli bez podawania parametrów,
  • chcemy uruchomić aplikację w trybie konsoli z dodatkowymi parametrami.

Analizując dalej możemy mieć do czynienie z następującymi sytuacjami:

  • aplikacja zostanie uruchomiona w oknie konsoli,
  • aplikacja zostanie uruchomiona z poza okna konsoli.

Otrzyma lista możliwych sytuacji od razu wskazuje, że powinniśmy zastanowić się nad dokładniejszą specyfikacją wymagań. Może okazać się, że niektóre funkcjonalności nie są potrzebne.

I rzeczywiście tak się okazało. Powyższa liczba przypadków została zredukowana do następujących dwóch scenariuszy:

  • w każdej sytuacji, gdy uruchamiamy aplikację bez parametrów uruchamia się ona we własnym oknie,
  • w sytuacji, gdy podane są jakiekolwiek parametry aplikacja musi odpalić się w konsoli.

Znając już dokładne wymagania można przejść do implementacji.

Pierwszą kwestią, z którą się spotykamy jest problem odczytu parametrów przekazywanych do programu. Jeśli otworzymy metodę Main() w programie WPF zobaczymy następujący kod:

/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static void Main()
{
    WpfApplication1.App app = new WpfApplication1.App();
    app.InitializeComponent();
    app.Run();
}

Od razu na myśl przychodzi pomysł, aby zmienić nagłówek metody na ten znany z programów pisanych na konsolę:

/// <summary>
/// Application Entry Point.
/// </summary>
[STAThreadAttribute]
[System.Diagnostics.DebuggerNonUserCodeAttribute]
public static void Main(string[] args)
{
    WpfApplication1.App app = new WpfApplication1.App();
    app.InitializeComponent();
    app.Run();
}

Jak się okazuje pomysł okazuje się trafiony. Po drodze napotkamy jeszcze na jeden problem z kompilacją. Otrzymamy następujący błąd:

Error 1 Program …\WpfApplication1\WpfApplication1\obj\x86\Debug\WpfApplication1.exe' has
more than one entry point defined: 'WpfApplication1.Program.Main()'. Compile with /main
 to specify the type that contains the entry point. …\WpfApplication1\WpfApplication1
\Program.cs 15 28 WpfApplication1

Error 2 Program …\WpfApplication1\WpfApplication1\obj\x86\Debug\WpfApplication1.exe' has
 more than one entry point defined: 'WpfApplication1.App.Main()'. Compile with /main
 to specify the type that contains the entry point. …\WpfApplication1\WpfApplication1
\obj\x86\Debug\App.g.cs 61 28 WpfApplication1

O tym jak poradzić sobie z tym błędem można przeczytać w innym moim wpisie – Zaginiona metoda Main()?

Po rozwiązaniu wszystkich problemów, szkielet metody Main(string[] args) wygląda następująco:

/// <summary>
/// Application Entry Point.
/// </summary>
[STAThreadAttribute]
[System.Diagnostics.DebuggerNonUserCodeAttribute]
public static void Main(string[] args)
{
    if (args.Length == 0)
    {
        WpfApplication1.App app = new WpfApplication1.App();
        app.InitializeComponent();
        app.Run();
    }
    else
    {
        // Missing code
    }
}

Jak widać połowa wymagań jest już spełniona – jeśli nie podamy jakichkolwiek parametrów uruchomi się program we własnym oknie. Pozostało tylko rozwiązanie problemu z oknem konsoli.

W przypadku konsoli mamy do czynienie z dwoma sytuacjami:

  • program odpalany jest z konsoli i wystarczy tylko podpiąć się do niej,
  • program nie jest odpalany z konsoli i należy otworzyć okno konsoli samodzielnie.

W celu rozwiązania tego problemu należy sprawdzić, w jaki sposób został uruchomiony program. Jeśli okaże się, że procesem aktywnym jest konsola to oznacza to, że program został uruchomiony z konsoli i wystarczy się pod nią podpiąć. W przeciwnym przypadku należy utworzyć okno konsoli:

// Get uppermost window process
IntPtr ptr = GetForegroundWindow();
int u;
GetWindowThreadProcessId(ptr, out u);
Process process = Process.GetProcessById(u);

// Check if it is console?
if (process.ProcessName == "cmd")
{
    // Yes – attach to active console
    AttachConsole(process.Id);
}
else
{
    // No – create new console
    AllocConsole();
}

// Program actions ...

FreeConsole();

Dodatkowo należy jeszcze zaimportować potrzebne funkcje:

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AllocConsole();

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FreeConsole();

[DllImport("kernel32", SetLastError = true)]
static extern bool AttachConsole(int dwProcessId);

[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

Po złożeniu wszystkiego w całość otrzymuje się następujący kod:

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AllocConsole();

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FreeConsole();

[DllImport("kernel32", SetLastError = true)]
static extern bool AttachConsole(int dwProcessId);

[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

/// <summary>
/// Application Entry Point.
/// </summary>
[STAThreadAttribute]
[System.Diagnostics.DebuggerNonUserCodeAttribute]
public static void Main(string[] args)
{
    if (args.Length == 0)
    {
        WpfApplication1.App app = new WpfApplication1.App();
        app.InitializeComponent();
        app.Run();
    }
    else
    {
        // Get uppermost window process
        IntPtr ptr = GetForegroundWindow();
        int u;
        GetWindowThreadProcessId(ptr, out u);
        Process process = Process.GetProcessById(u);

        // Check if it is console?
        if (process.ProcessName == "cmd")
        {
            // Yes – attach to active console
            AttachConsole(process.Id);
        }
        else
        {
            // No – create new console
            AllocConsole();
        }

        // Program actions ...

        FreeConsole();
    }
}