WPF-Tricks: Kontext-Menü per Behavior

Behaviors bieten in WPF die Möglichkeit ein Stück Funktionalität in einer wiederverwendbare Komponente zu verpacken. Behaviors erlauben Anwendungsdesignern – mit wenig Aufwand – Änderungen am Programmverhalten zu bewirken ohne in den Programmcode einzugreifen. Die Entwickler stellen dafür beispielsweise Behaviors für Drag & Drop oder Touch-Interaktion bereit, die dann im “Blend” einfach auf das entsprechende Element angewandt werden. Dieses Tutorial zeigt, wie sich mit Hilfe eines Behaviors ein Kontext-Menü erzeugen lässt.

Das Ziel

Die Inhalte eine Liste sollen vom Nutzer kopiert werden können. Da wir die einzelnen Listenelemente nicht in editierbare Text-Boxen umwandeln möchten, soll ein Kontextmenü das Kopieren möglich machen:

Menüpunkt mit Text "Inhalt kopieren" und Mauszeiger darauf
Abbildung 1: Kontext-Menü zum Kopieren des Inhalts

So geht’s

Wir erstellen ein neues Behavior indem wir von Behavior<T> ableiten. Das Blend-Behavior findet sich in System.Windows.Interactivity. Als Typ geben wir FrameworkElement an – damit funktioniert unser Behavior auf praktisch allen Controls:

    public class InhaltKopierenBahavior : Behavior<FrameworkElement>

Als erstes überschreiben wir OnAttached() und OnDetaching(). Hier können wir reagieren, wenn ein Behavior hinzugefügt oder entfernt wird.

    protected override void OnAttached()
    {
     base.OnAttached();
    }
    protected override void OnDetaching()
    {
     base.OnDetaching();
    }

Das Behavior bietet uns Zugriff auf das Element, an das es ‘attached’ wurde. Dafür gibt es das Property AssociatedObject. Das AssociatedObject ist immer vom Typ, der bei der Klassendefintion angegeben wurde. (hier FrameworkElement). An das AssociatedObject ‘hängen’ wir jetzt unser Kontext-Menü mit den gewünschten Optionen. In unserem Beispiel reicht uns der Befehl “Inhalt kopieren”. Dieser soll den angeklickten Text in die Zwischenablage kopieren. Im Beispiel wird der Menüeintrag auch gleich noch mit einem Icon versehen:

    protected override void OnAttached()
    {
     base.OnAttached();
                
     var uri = new Uri("copy-icon-256.png");
     var logo = new BitmapImage();
     logo.BeginInit();
     logo.DecodePixelWidth = 16;
     logo.UriSource = uri;
     logo.EndInit();
     var menuEintrag = new MenuItem
     {
      Header = "Inhalt kopieren",
      Icon = new Image { Source = logo }
     };
     menuEintrag.Click += MenuEintragOnClick;
     AssociatedObject.ContextMenu = new ContextMenu();
     AssociatedObject.ContextMenu.Items.Add(menuEintrag);
     AssociatedObject.PreviewMouseRightButtonDown += MouseDown;
    }

Das Element, zu dem wir unser Kontext-Menü hinzufügen ist nicht zwingend das Element, dessen Inhalt wir kopieren wollen. Wir wollen ja den Inhalt eines ListViewItems und nicht den gesamten ListView kopieren. Das Behavior auf jedes Listenelement anzuwenden, wäre aber unverhältnismäßig mühevoll. Dem AssociatedObject fügen wir deshalb noch einen Event-Handler hinzu. Beim Rechtsklick soll nicht nur das Menü geöffnet, sondern zugleich auch das “angeklickte” Element ermittelt werden. Das “getroffene” Element speichern wir uns in dem Field ‘_element’.

    private void MouseDown(object sender, MouseButtonEventArgs args)
    {
     _element = args.MouseDevice.DirectlyOver;
     args.Handled = true;
    }

Nun setzen wir die eigentliche Kopier-Funktionalität um, dafür bekommt der Menü-Eintrag einen entsprechenden Event-Handler. Wir gehen davon aus, dass das getroffene Element ein Text-Block ist. Wenn nicht, können wir ohnehin nichts sinnvoll in die Zwischenablage kopieren.

    private void MenuEintragOnClick(object sender, RoutedEventArgs args)
    {
     var textBlock = _getroffenesElement as TextBlock;
     if (textBlock != null)
     {
      Clipboard.SetDataObject(textBlock.Text);
     }
    }

Zu guter Letzt wenden wir das Behavior in unserer Anwendung an. Wichtig ist, dass auch der “Blend” Namespace eingebunden ist: xmlns:i=”http://schemas.microsoft.com/expression/2010/interactivity”

    <ListView>
     <i:Interaction.Behaviors>
      <behaviors:InhaltKopierenBahavior/>
     </i:Interaction.Behaviors>
    </ListView>

WPF-Tricks: Icons zur Laufzeit färben

Icons helfen Nutzern sich in einer Anwendungsoberfläche zurecht zu finden. Gut gemachte Icons sind schnell erfassbar und leisten damit einen wichtigen Beitrag zur intuitiven Bedienbarkeit. Sie gehören daher in jede Anwendung – können Entwickler aber auch vor unerwartete Probleme stellen.

Das Problem

Auf einem Button soll ein Icon erscheinen. Je nach Zustand (normal, hover, disabled) sollen sich Vorder- und Hintergrundfarbe des Controls ändern und damit auch die Farbe des Icons. In meinem Beispiel sorgt die relativ dunkle Hintergrundfarbe beim Hover dafür, dass die Farbe des Icons von schwarz zu weiß wechseln muss. Da die Hintergrundfarbe durch die CI des Kunden vorgegeben wird, lässt sie sich auch nicht ohne Weiteres ändern.

Abbildung 1: Je nach Zustand (normal, hover, disabled) soll auch die Farbe des Icons wechseln

Üblicherweise braucht es jetzt verschiedene Icon-Sets und die Grafiken müssen zur Laufzeit ausgetauscht werden. Wäre es nicht toll, man könnte die Icons einfach zur Laufzeit beliebig einfärben?

So geht’s

Wir ersetzen das Control-Template des Buttons per Style. Der eigentliche Inhalt wird dabei hinter einem Border und einem Rectangle versteckt. Wichtig ist, dass beide genau so groß wie der originale Content sind. Die Hintergrundfarbe des Borders wird per Template-Binding auf die Hintergrundfarbe des Buttons gebunden. Die Füllfarbe des Rectangles binden wir auf die Vordergrundfarbe. In dieser Farbe wird dann der Inhalt des Buttons sichtbar.

Den jetzt verdeckten Content-Presenter des Buttons verwenden wir, mit Hilfe eines Visualbrush, als Opacity-Mask für das Rectangle. Das Rectangle wird nun “ausgestanzt”: An den Stellen, an denen der Content sichtbar ist, bleibt das Rectangle ebenfalls sichtbar. An allen anderen wird es transparent. Der Inhalt des Buttons wird also in der gesetzten Vordergrundfarbe sichtbar!

Abbildung 2: Der Inhalt des Buttons wird in der gesetzten Vordergrundfarbe sichtbar.

Hier eine gekürzte Version des Control-Templates (einige Properties fehlen):

    <Border
     Background="{TemplateBinding Background}" 
     BorderBrush="{TemplateBinding BorderBrush}" 
     BorderThickness="{TemplateBinding BorderThickness}">
     <Grid>
      <ContentPresenter x:Name="MyContentPresenter" 
                Content="{TemplateBinding Content}"/>
      <Border Background="{TemplateBinding Background}">
       <Rectangle 
        Fill="{TemplateBinding Foreground}" 
        Width="{Binding ElementName=MyContentPresenter, Path=ActualWidth}" 
        Height="{Binding ElementName=MyContentPresenter, Path=ActualHeight}">
        <Rectangle.OpacityMask>
         <VisualBrush Stretch="None" Visual="{Binding ElementName=MyContentPresenter}"/>
        </Rectangle.OpacityMask>
       </Rectangle>
      </Border>
     </Grid>
    </Border>

Der Button kann weiterhin mit beliebigem Content befüllt werden:

    <Button>
     <StackPanel Orientation="Horizontal">
      <Image  Source="{StaticResource Icon}" Margin="2" />
      <TextBlock VerticalAlignment="Center">los gehts!</TextBlock>
     </StackPanel>
    </Button>

(Am Ende des Artikels findet befindet sich der komplette Style)

Fazit

Mit diesem kleinen Trick ist es möglich, Icons zur Laufzeit praktisch beliebig einzufärben. Verschiedenfarbige Icon-Sets werden damit unnötig. Das Prinzip ist sehr leichtgewichtig, hat aber auch Einschränkungen: Graustufige oder Mehrfarbige Icons, lassen sich bspw. nicht verwenden.

Das Prinzip ist natürlich nicht nur für Buttons sinnvoll, sondern kann auch bei beliebigen anderen Controls Anwendung finden. Durch das Binding der Farbe wären letztendlich sogar Farbverläufe oder gar Animationen machbar.

Schriftzug "Ende"

Der komplette Style:

    <Style TargetType="Button">
     <Setter Property="Background" Value="WhiteSmoke" />
     <Setter Property="Foreground" Value="Black" />
     <Setter Property="SnapsToDevicePixels" Value="True" />
     <Setter Property="FontWeight" Value="Normal"/>
     <Setter Property="MinWidth" Value="50"/>
     <Setter Property="Padding" Value="1"/>
     <Setter Property="BorderBrush" Value="LightGray"/>
     <Setter Property="BorderThickness" Value="1"/>
     <Setter Property="VerticalContentAlignment" Value="Center"/>
     <Setter Property="HorizontalContentAlignment" Value="Center"/>
     <Setter Property="Template">
      <Setter.Value>
       <ControlTemplate TargetType="{x:Type Button}">
        <Border
         Background="{TemplateBinding Background}" 
         BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}" 
         MinWidth="{TemplateBinding MinWidth}">
         <Grid>
          <ContentPresenter x:Name="MyContentPresenter" 
           Content="{TemplateBinding Content}" 
           HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
           VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
           Margin="{TemplateBinding Padding}"/>
          <Border Background="{TemplateBinding Background}">
           <Rectangle 
            Fill="{TemplateBinding Foreground}" 
            Margin="{TemplateBinding Padding}"
            Width="{Binding ElementName=MyContentPresenter, Path=ActualWidth}" 
            Height="{Binding ElementName=MyContentPresenter, Path=ActualHeight}" 
            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
            VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
            <Rectangle.OpacityMask>
             <VisualBrush Stretch="None" Visual="{Binding ElementName=MyContentPresenter}"/>
            </Rectangle.OpacityMask>
           </Rectangle>
          </Border>
         </Grid>
        </Border>
        <ControlTemplate.Triggers>
         <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="{DynamicResource Brushes50HzOrange158}" />
          <Setter Property="Foreground" Value="White" />
         </Trigger>
         <Trigger Property="IsPressed" Value="True">
          <Setter Property="Background" Value="{DynamicResource Brushes50HzRotOrange179}" />
          <Setter Property="Foreground" Value="White" />
         </Trigger>
         <Trigger Property="IsEnabled" Value="False">
          <Setter Property="Foreground" Value="{DynamicResource Brushes50HzGrau430}"/>
         </Trigger>
        </ControlTemplate.Triggers>
       </ControlTemplate>
      </Setter.Value>
     </Setter>
    </Style>