Entwickler-Ecke

WinForms - C# - Eigene einfache Fensterklasse erstellen


Kasko - Sa 26.01.19 23:34
Titel: C# - Eigene einfache Fensterklasse erstellen
In C++ ist es ja relativ einfach ein einfaches Fenster mithilfe der Win32-API zu erstellen (wWinMain,WndProc,...).
Ist dies auch in C# möglich ohne Formulare zu verwenden. Es soll wirklich nur ein ganz einfaches Fenster sein.


Christian S. - So 27.01.19 00:47

Du kannst natürlich jede Win32-API-Funktion auch in .NET aufrufen (mittels PInvoke). Aber so richtig sinnvoll klingt das nicht. Was möchtest Du denn letzten Endes damit erreichen?


Palladin007 - So 27.01.19 05:09

"ohne Formulare" und "wirklich nur ein ganz einfaches Fenster" wiederspricht sich ;)

Du kannst natürlich WPF verwenden, da heißen die Fenster nicht mehr "Formular" sondern "Window" :D
Oder ASP.NET (Core), da ist's dann halt eine Website

Welche der drei Optionen (WinForms, WPF, WinAPI, APS.NET) Du verwenden willst, die WinAPI ist vermutlich die am wenigsten einfache Option ;)
Natürlich musst Du dich dazu erst einmal damit auseinander setzen, für einen Umsteiger von C** mag das etwas umständlich und ungewohnt sein, aber glaube mir: Du sparst dir damit sehr viel Arbeit und Nerven. Abgesehen davon sind WinForms und WPF (ohne MVVM, ist aber nicht empfohlen!) sehr einfach in der Anwendung, ein einfaches Fenster mit TextBox und Button kriegst Du in fünf Minuten auf die Beine gestellt.


Delete - So 27.01.19 18:36

- Nachträglich durch die Entwickler-Ecke gelöscht -


Th69 - So 27.01.19 18:51

Dann wäre es wohl besser, anstatt per P/Invoke die ganzen WinAPI-Funktionen zu benutzen, ein C++/CLI-Projekt (Assembly) dafür zu erstellen und direkt per C++ diese Funktionen aufzurufen.


Palladin007 - Mo 28.01.19 03:48

Wäre es da nicht besser, eine normale WinForms-Anwendung zu bauen und dann ein Konsolen-Fenster auf zu machen?
Ich meine mich zu erinnern, dass man über die WinAPI und recht geringem Aufwand sowas machen kann - ist allerdings schon eine Weile her, sicher bin ich mir daher nicht.


Delete - Sa 02.02.19 06:43

- Nachträglich durch die Entwickler-Ecke gelöscht -


Palladin007 - Sa 02.02.19 13:33

Eine überschaubare, kleine Klasse (und eine kleine Helfer-Klasse mit den Kernel32-Methoden):


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
public static class Kernel32
{
    private const string DLL = "kernel32.dll";
        
    [DllImport(DLL, SetLastError = true, ExactSpelling = true)]
    public static extern bool FreeConsole();

    [DllImport(DLL)]
    public static extern bool AllocConsole();

    [DllImport(DLL, CharSet = CharSet.Auto)]
    public static extern IntPtr CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);

    [DllImport(DLL)]
    public static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);
}

public static class WinConsole
{
    private static FileStream _inStream;
    private static FileStream _outStream;

    public static bool IsAttached { get; private set; }

    public static void Open()
    {
        IsAttached = Kernel32.AllocConsole();

        if (IsAttached)
        {
            InitInStream();
            InitOutStream();
        }
    }
    public static void Close()
    {
        if (!IsAttached)
            return;

        Kernel32.CancelIoEx(_inStream.SafeFileHandle.DangerousGetHandle(), IntPtr.Zero);
        Kernel32.CancelIoEx(_outStream.SafeFileHandle.DangerousGetHandle(), IntPtr.Zero);
        Kernel32.FreeConsole();

        _inStream.Dispose();
        _outStream.Dispose();
    }

    private static void InitInStream()
    {
        _inStream = CreateFileStream("CONIN$", GENERIC_READ, FILE_SHARE_READ, FileAccess.Read);

        if (_inStream != null)
            Console.SetIn(new StreamReader(_inStream));
    }
    private static void InitOutStream()
    {
        _outStream = CreateFileStream("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, FileAccess.Write);

        if (_outStream != null)
        {
            var writer = new StreamWriter(_outStream) { AutoFlush = true };

            Console.SetOut(writer);
            Console.SetError(writer);
        }
    }

    private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode, FileAccess dotNetFileAccess)
    {
        var file = new SafeFileHandle(Kernel32.CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero), true);

        return file.IsInvalid
            ? null
            : new FileStream(file, dotNetFileAccess);
    }
        
    private const uint GENERIC_WRITE = 0x40000000;
    private const uint GENERIC_READ = 0x80000000;
    private const uint FILE_SHARE_READ = 0x00000001;
    private const uint FILE_SHARE_WRITE = 0x00000002;
    private const uint OPEN_EXISTING = 0x00000003;
    private const uint FILE_ATTRIBUTE_NORMAL = 0x80;
}


Ein Nutzungs-Beispiel:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
public partial class Form1 : Form
{
    private readonly TextBox _consoleTextBox;
    private CancellationTokenSource _readConsoleCancelTokenSource;
    private Task _readConsoleTask;

    public Form1()
    {
        InitializeComponent();

        Controls.Add(_consoleTextBox = new TextBox()
        {
            Multiline = true,
            Dock = DockStyle.Fill,
        });
    }

    protected override void OnLoad(EventArgs e)
    {
        WinConsole.Open();

        _readConsoleCancelTokenSource = new CancellationTokenSource();
        _readConsoleTask = ReadConsoleAsync(_readConsoleCancelTokenSource.Token);

        base.OnLoad(e);
    }
    protected override async void OnClosing(CancelEventArgs e)
    {
        _readConsoleCancelTokenSource.Cancel();
        WinConsole.Close();
        await _readConsoleTask;
        _readConsoleCancelTokenSource.Dispose();

        base.OnClosing(e);
    }

    private async Task ReadConsoleAsync(CancellationToken cancelToken)
    {
        if (cancelToken.IsCancellationRequested)
            return;

        var line = await Task.Run(() =>
        {
            try
            {
                Console.Write("Write something: ");
                return Console.ReadLine();
            }
            catch (OperationCanceledException)
            {
                return null;
            }

        }, cancelToken);

        if (cancelToken.IsCancellationRequested)
            return;

        if (!string.IsNullOrEmpty(line))
            _consoleTextBox.AppendText(line + Environment.NewLine);

        await ReadConsoleAsync(cancelToken);
    }
}


Das Fehler-Handling kann man sicher noch optimieren und ob alles sauber aufgeräumt wird, kann ich auch nicht sagen, aber es funktioniert.

Eine in sich abgeschlossene Klasse, die dafür sorgt, dass eine WinForms-Anwendung eine funktionierende und nutzbare Konsole bekommt - WinForms funktioniert weiterhin.
Ich verstehe nicht ganz, warum es vernünftiger ist, das Window über die WinAPI per Hand nachzubauen - das was WinForms schon vollständig übernimmt. Ich bastel mir lieber eine Konsole nach, die nicht so aufwändig zu nutzen ist.

PS:
Getestet auf Win10 x64. Eine Garantie für ältere Versionen kann ich natürlich nicht bieten.


Delete - Sa 02.02.19 17:26

- Nachträglich durch die Entwickler-Ecke gelöscht -


Palladin007 - Sa 02.02.19 18:58

Das ist entstanden, indem ich ein bisschen gegoogelt habe :D
Keine Garantie auf Vollständigkeit oder sauberen Code ^^

Das dient nur als Beispiel, wie einfach es ist, eine Konsole zu einer WinForms- oder WPF-Anwendung dazu zu holen und nutzbar zu machen.


Wobei es vielleicht auch eine Idee ist, einfach eine zweite Konsolen-App daneben zu stellen und die kommuniziert dann mit der WinForms- oder WPF-Anwendung.
Das ist nicht viel komplexer als das hier, wenn man z.B. WCF nutzt, denn wenn man eine Konsole dazu holt. Und man spart sich die WinApi


Delete - Sa 02.02.19 19:11

- Nachträglich durch die Entwickler-Ecke gelöscht -