Zum Inhalt
Kuonen.UI

Dateiauswahl

Dateien öffnen und speichern und Ordner wählen mit dem Dialog der Plattform: Windows (Common Item Dialog), Linux (xdg-desktop-portal), macOS (NSOpenPanel, NSSavePanel), Android (Storage Access Framework), iOS (Dokumentauswahl) und Browser (File System Access, sonst Hochladen und Herunterladen). Gewählte Dateien sind Ströme; einen Pfad gibt es nur, wo die Plattform einen kennt. Abbrechen liefert eine leere Liste. Im Headless-Host antwortet ein Drehbuch.

Vorführung: DateiauswahlVorführung: Dateiauswahl

KXAML

DateiauswahlSeite.kxaml
<StackPanel xmlns="https://ui.kuonen.org/kxaml"
            xmlns:x="https://ui.kuonen.org/kxaml/x"
            x:Class="Kuonen.UI.Galerie.Seiten.DateiauswahlVorfuehrung"
            Spacing="12">
  <TextBlock Text="Dialog der Plattform" FontWeight="SemiBold" />
  <CheckBox x:Name="Mehrfach" Content="Mehrere Dateien" />
  <StackPanel Orientation="Horizontal" Spacing="8">
    <Button x:Name="Oeffnen" Content="Öffnen" Style="{x:Static Fluent.AkzentButton}" Click="Oeffnen_Click" />
    <Button x:Name="Speichern" Content="Speichern" Click="Speichern_Click" />
    <Button x:Name="Ordner" Content="Ordner" Click="Ordner_Click" />
  </StackPanel>
  <TextBlock x:Name="Ergebnis" Text="Noch nichts gewählt." TextWrapping="Wrap" />
</StackPanel>
DateiauswahlSeite.kxaml.cs
using Kuonen.UI;
using Kuonen.UI.Ereignisse;

namespace Kuonen.UI.Galerie.Seiten;

public sealed partial class DateiauswahlSeite
{
    protected override Func<UIElement>? KxamlFabrik => () => new DateiauswahlVorfuehrung();
}

/// <summary>KXAML-Fassung der Dateiauswahl: der Baum steht in DateiauswahlSeite.kxaml, hier nur das Verhalten.</summary>
public sealed partial class DateiauswahlVorfuehrung
{
    public DateiauswahlVorfuehrung() => InitializeComponent();

    private async void Oeffnen_Click(object sender, RoutedEventArgs e) =>
        await DateiauswahlSeite.Zeige(Ergebnis, () => DateiauswahlSeite.OeffneAsync(Oeffnen, Mehrfach.IsChecked == true));

    private async void Speichern_Click(object sender, RoutedEventArgs e) => await DateiauswahlSeite.Zeige(Ergebnis, () => DateiauswahlSeite.SpeichereAsync(Speichern));

    private async void Ordner_Click(object sender, RoutedEventArgs e) => await DateiauswahlSeite.Zeige(Ergebnis, () => DateiauswahlSeite.WaehleOrdnerAsync(Ordner));
}

C#

DateiauswahlSeite.cs
using System.Text;
using Kuonen.UI;
using Kuonen.UI.Controls;
using Kuonen.UI.Dienste;
using Kuonen.UI.Layout;
using Kuonen.UI.Themes;

namespace Kuonen.UI.Galerie.Seiten;

/// <summary>Galerie-Seite: Dateien öffnen und speichern, Ordner wählen über den Dialog der Plattform (Plan 6h).</summary>
public sealed partial class DateiauswahlSeite : GalerieSeite
{
    /// <summary>Was „Speichern“ in die gewählte Datei schreibt.</summary>
    public const string Inhalt = "Gespeichert aus der Kuonen.UI-Galerie.";

    public override string Titel => "Dateiauswahl";

    public override string Beschreibung =>
        "Dateien öffnen und speichern und Ordner wählen mit dem Dialog der Plattform: Windows (Common Item Dialog), Linux (xdg-desktop-portal), "
        + "macOS (NSOpenPanel, NSSavePanel), Android (Storage Access Framework), iOS (Dokumentauswahl) und Browser (File System Access, sonst "
        + "Hochladen und Herunterladen). Gewählte Dateien sind Ströme; einen Pfad gibt es nur, wo die Plattform einen kennt. Abbrechen liefert "
        + "eine leere Liste. Im Headless-Host antwortet ein Drehbuch.";

    public override string Stichworte => "Dateiauswahl Datei öffnen speichern Ordner Dialog IDateiauswahl Window.Dateiauswahl Dateityp OeffneAsync SpeichereAsync WaehleOrdnerAsync";

    public override string Datei => nameof(DateiauswahlSeite);

    private static IDateiauswahl? Dienst(Visual element) => MessageBoxSeite.FensterVon(element)?.Dateiauswahl;

    /// <summary>Öffnet eine oder mehrere Dateien und beschreibt das Ergebnis.</summary>
    internal static async Task<string> OeffneAsync(Visual element, bool mehrfach)
    {
        if (Dienst(element) is not { KannOeffnen: true } auswahl) return "Dieser Host bietet keine Dateiauswahl.";
        var dateien = await auswahl.OeffneAsync(new DateiOeffnenOptionen
        {
            Titel = "Datei öffnen", Mehrfach = mehrfach, Dateitypen = [Dateityp.Bilder, Dateityp.Text, Dateityp.Alle],
        });
        if (dateien.Count == 0) return "Abgebrochen.";
        var text = new StringBuilder(dateien.Count == 1 ? "Gewählt: " : $"{dateien.Count} Dateien: ");
        for (int i = 0; i < dateien.Count && i < 20; i++)
        {
            if (i > 0) text.Append(", ");
            text.Append(dateien[i].Name);
            if (dateien[i].Groesse is { } groesse) text.Append(" (").Append(groesse).Append(" Bytes)");
        }
        return text.ToString();
    }

    /// <summary>Lässt eine Datei wählen und schreibt <see cref="Inhalt"/> hinein.</summary>
    internal static async Task<string> SpeichereAsync(Visual element)
    {
        if (Dienst(element) is not { KannSpeichern: true } auswahl) return "Dieser Host bietet keine Dateiauswahl.";
        var datei = await auswahl.SpeichereAsync(new DateiSpeichernOptionen { Titel = "Speichern unter", Vorschlag = "Notiz.txt", Dateitypen = [Dateityp.Text] });
        if (datei is null) return "Abgebrochen.";
        await using (var strom = await datei.OeffneSchreibenAsync())
        {
            var bytes = Encoding.UTF8.GetBytes(Inhalt);
            await strom.WriteAsync(bytes);
        }
        return "Gespeichert: " + datei.Name;
    }

    /// <summary>Lässt einen Ordner wählen.</summary>
    internal static async Task<string> WaehleOrdnerAsync(Visual element)
    {
        if (Dienst(element) is not { KannOrdnerWaehlen: true } auswahl) return "Dieser Host kann keine Ordner wählen.";
        var ordner = await auswahl.WaehleOrdnerAsync(new OrdnerOptionen { Titel = "Ordner wählen" });
        return ordner.Count == 0 ? "Abgebrochen." : "Ordner: " + ordner[0].Name;
    }

    // Eine Ausnahme der Plattform (etwa ohne Portal) erscheint als Text, statt die Anwendung anzuhalten.
    internal static async Task Zeige(TextBlock ergebnis, Func<Task<string>> aktion)
    {
        try { ergebnis.Text = await aktion(); }
        catch (Exception e) when (e is InvalidOperationException or IOException or UnauthorizedAccessException or NotSupportedException)
        {
            ergebnis.Text = "Nicht möglich: " + e.Message;
        }
    }

    public override UIElement Baue()
    {
        var mehrfach = new CheckBox { Content = "Mehrere Dateien" };
        var ergebnis = new TextBlock { Text = "Noch nichts gewählt.", TextWrapping = TextWrapping.Wrap };
        var oeffnen = new Button { Content = "Öffnen", Style = Fluent.AkzentButton };
        var speichern = new Button { Content = "Speichern" };
        var ordner = new Button { Content = "Ordner" };
        oeffnen.Click += async (_, _) => await Zeige(ergebnis, () => OeffneAsync(oeffnen, mehrfach.IsChecked == true));
        speichern.Click += async (_, _) => await Zeige(ergebnis, () => SpeichereAsync(speichern));
        ordner.Click += async (_, _) => await Zeige(ergebnis, () => WaehleOrdnerAsync(ordner));

        var knoepfe = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
        knoepfe.Children.Add(oeffnen);
        knoepfe.Children.Add(speichern);
        knoepfe.Children.Add(ordner);
        var seite = new StackPanel { Spacing = 12 };
        seite.Children.Add(new TextBlock { Text = "Dialog der Plattform", FontWeight = 600 });
        seite.Children.Add(mehrfach);
        seite.Children.Add(knoepfe);
        seite.Children.Add(ergebnis);
        return seite;
    }
}