.NET-Color-Picker für WPF- und Avalonia-Applikationen
Best of NuGet, Teil 7
Die Realisierung eines Farbauswahldialogs ist eine auf den ersten Blick einfach erscheinende Aufgabe, die in der Praxis aber eine ganze Reihe von Hürden bereithält. ColorPicker kann seine Stärken immer dann ausspielen, wenn ein präziser, an Photoshop und Co. erinnernder Farbauswahldialog erforderlich ist. Wer dagegen in einer Purikura-zentrierten Applikation Farben auswählen möchte, ist mit anderen Benutzerinterfaces besser bedient.
Android- beziehungsweise MAUI-Alternative verfügbar
Wer abseits des Avalonia-Frameworks Mobilapplikationen entwickeln möchte, sollte eine andere Color-Picker-Bibliothek verwenden. Der Autor hat in seinem Unternehmen mit https://github.com/nor0x/Maui.ColorPicker gute Erfahrungen gemacht.
Inbetriebnahme mit einer WPF-Applikation
Alle Innovationen in Redmond konnten die WPF bisher nicht eliminieren – in der Praxis gilt, dass die Arbeit mit WPF-Applikationen nach wie vor sehr bequem ist. Funktionen wie der WYSIWYG-Designer ermöglichen eine immens hohe Produktivität.
Als erste Inbetriebnahme-Aufgabe wollen wir eine Applikation auf Basis der Vorlage WPF-App (.NET Framework) ins Leben rufen. Als Name der Solution wählen wir in den folgenden Schritten den String dnpColorPickerWPF aus und entscheiden uns für das .NET Framework in der Version 4.8.
Danach wechseln wir in den NuGet-Paketmanager, wo wir nach dem String PixiEditor.ColorPicker suchen. Zum Zeitpunkt der Abfassung dieses Artikels war die Version 3.4.2.3 der Bibliothek aktuell. Im nächsten Schritt wechseln wir in die XAML-Datei, in der eine nach folgendem Schema aufgebaute Namespace-Deklaration unterkommt:
<Window x:Class="dnpCplorPickerWPF.MainWindow"
xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
Im nächsten Schritt erfolgt das Einpflegen der Steuerelement-Instanz nach folgendem Schema:
<Grid>
<colorpicker:StandardColorPicker x:Name="main" Width="200" Height="380"/>
</Grid>
Hervorzuheben ist, dass das Paket in den meisten Fällen – die Framework Version 4.7 ist ausgenommen – im Vorschaufenster eine halbwegs funktionale Version des Farbauswahldialogs rendert. Außerdem sind die diversen in Visual Studio enthaltenen Editor-Funktionen scharfgeschaltet.
Das bedeutet, dass das Hinzufügen eines durch ColorChanged="main_ColorChanged" deklarierbaren Eventhandlers entweder durch Code-Bearbeitung oder aber über den Events-Tab der Steuerelement-Vorschau erfolgen kann. Die Syntax des Handlers präsentiert sich im leeren Zustand folgendermaßen:
private void main_ColorChanged(object sender, RoutedEventArgs e) {
}Auswertung der Farbdaten
An dieser Stelle ist das Programm in der Theorie zur Ausführung bereit. Interessanter ist es allerdings, die im Picker abgelegten Farbinformationen in einem weiteren Steuerelement sichtbar zu machen.
Das XAML-Rechteck lässt sich mit einem Hintergrund ausstatten und ist ein bequemer Ausgabe-Kandidat. Erster Akt ist das Realisieren eines LinearLayout im Grid-Steuerelement, was nach folgendem Schema aufgebaute Anpassungen in der XAML-Struktur voraussetzt:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<colorpicker:StandardColorPicker Grid.Row="0" x:Name="CprMyPicker" Width="200" Height="380" ColorChanged="main_ColorChanged"/>
<Rectangle Grid.Row="1" Width="800" Height="50" x:Name="RctMyRect"></Rectangle>
</Grid>
Im nächsten Schritt erfolgt die Extraktion der Farbwerte im Eventhandler. In der Theorie könnte hier auch XAML-Data-Binding verwendet werden, die vorliegende Version erlaubt allerdings direkten Zugriff in die Datenfelder:
private void main_ColorChanged(object sender, RoutedEventArgs e) {
RctMyRect.Fill = new SolidColorBrush(CprMyPicker.SelectedColor);
}
Die Ausführung des Programms führt dazu, dass Änderungen an den diversen Einstellungsreglern im Rechteck wiedergegeben werden (Bild 1). Zu beachten ist, dass das Ziehen beziehungsweise Gedrückthalten der Maus beim Anpassen der Werte zu einer permanenten Änderung der Farbe des Rechtecks führt. Bei sehr komplexen XAML-Strukturen kann es empfehlenswert sein, eine Verzögerung zwischen die Events zu schalten.
Die Farb-Aktualisierung des Rechtecks funktioniert problemlos (Bild 1)
AutorAnalyse der im Hintergrund zur Verfügung stehenden Datenstrukturen
Über Farbmanagement beziehungsweise die verschiedenen Arten, Farben zu repräsentieren, lassen sich ganze Lehrbücher schreiben. Im ersten Schritt wollen wir das Attribut CprMyPicker.Color „reflektieren“:
private void main_ColorChanged(object sender, RoutedEventArgs e)
{
var victim = CprMyPicker.Color;
Color ist dabei ein Universal-Attribut, in dem die Bibliothek Informationen über die vom Benutzer ausgewählten Farben in mehreren Formaten bereitstellt.
Auf der ersten Ebene findet sich dabei die folgende Container-Struktur, die vor allem für das Zurverfügungstellen einer Anlaufstelle für Change-Listener verantwortlich ist:
public class NotifyableColor : NotifyableObject
{
private readonly IColorStateStorage storage;
Die eigentliche Darstellung erfolgt dann in einem Objekt vom Typ ColorState. Interessant ist bei der Reflexion vor allem, dass nach folgendem Schema drei Daten-Komplemente enthalten sind:
private double _RGB_R; private double _RGB_G; private double _RGB_B; private double _HSV_H; private double _HSV_S; private double _HSV_V; private double _HSL_H; private double _HSL_S; private double _HSL_L;
Intern enthält die Bibliothek dann noch einige Konverter-Routinen, die zwischen den verschiedenen Farbdarstellungen makeln können. Für Entwickler ist dies unter anderem deshalb sinnvoll, weil sie die eingestellten Parameter in jenem Format abernten können, das für die vorliegende Aufgabe am besten geeignet erscheint.
Nutzung weiterer Darbietungsformen des Color Pickers
Der hier verwendete und vergleichsweise umfangreiche Dialog ist nur ein Beispiel für die in der Bibliothek enthaltenen Optionen. Die einfachste Alternative ist eine kompaktere Version, die die eigentliche Logik beziehungsweise Auswahl-Schnittstelle nur noch auf explizite Anfrage anzeigt. Zu ihrer Nutzung ist es notwendig, das XAML-Markup nach dem folgenden Schema zu ergänzen:
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<colorpicker:PortableColorPicker Grid.Row="0" x:Name="CprMyPicker" Width="50" Height="50" ColorChanged="main_ColorChanged"/>
<Rectangle Grid.Row="1" Width="800" Height="50" ></Rectangle>
<Rectangle Grid.Row="2" Width="800" Height="50" x:Name="RctMyRect"></Rectangle>
Der Trigger, der die eigentliche Auswahllogik ausklappt, präsentiert sich dabei wie ein farbiges Quadrat. Bild 2 zeigt das Programm in Aktion.
Der Color Picker im ausgeklappten Zustand (Bild 2)
AutorIn der Bibliothek findet sich ein gutes halbes Dutzend unterschiedlichster Steuerelemente. Interessant ist die Möglichkeit, diese miteinander zu verknüpfen. Hierzu steht das Attribut ColorState zur Verfügung. Es exponiert den kompletten internen Zustand eines Steuerelements und ist explizit nur für die Verknüpfung unter Nutzung von Data Binding vorgesehen.
Für einen ersten Versuch wollen wir nach dem folgenden Schema einen gewöhnlichen und einen kompakten Color Picker in einem Formular vereinigen:
<colorpicker:PortableColorPicker Grid.Row="0" x:Name="CprMyPicker" Width="50" Height="50" ColorState="{Binding ElementName=CprMyLargePicker, Path=ColorState, Mode=TwoWay}"/>
<colorpicker:StandardColorPicker Grid.Row="1" x:Name="CprMyLargePicker" Width="200" Height="380" />
<Rectangle Grid.Row="2" Width="800" Height="50" x:Name="RctMyRect"></Rectangle>
Wer diese Version des Programms zur Ausführung bringt, stellt fest, dass die in den beiden Pickern enthaltenen Werte permanent miteinander abgeglichen werden.
Derartige Bindungen lassen sich beliebig komplex gestalten. Das folgende Snippet würde beispielsweise einen kompakten, einen gewöhnlichen und einen schieberbasierten Auswahldialog miteinander verbinden:
<colorpicker:PortableColorPicker Grid.Row="0" x:Name="CprMyPicker" Width="50" Height="50" ColorState="{Binding ElementName=CprMyLargePicker, Path=ColorState, Mode=TwoWay}"/>
<colorpicker:StandardColorPicker Grid.Row="1" x:Name="CprMyLargePicker" Width="200" Height="380" />
<colorpicker:ColorSliders Grid.Row="2" x:Name="CprMySlide" Width="50" Height="50" ColorState="{Binding ElementName=CprMyLargePicker, Path=ColorState, Mode=TwoWay}"/>Fazit
Wer in einer Avalonia- oder WPF-Applikation einen Farbauswahldialog benötigt, findet in PixiEditor.ColorPicker eine an Photoshop und Co. erinnernde Implementierung. Gegenüber der manuellen Programmierung spart man sich mit Sicherheit die eine oder andere Mannstunde; außerdem kümmert sich die Bibliothek um Farbraumkonversionen. Unterm Strich also ein wertvoller Baustein des NuGet-Ökosystems.