Visual Studio 2012 Metro Styles für WPF–Metro Window Update

Der letzte Schliff für meine Visual Studio 2012 Metro Styles ist natürlich das MetroWindow, was dann wirklich die ganze Anwendung im typischen Grauton erscheinen lässt. Glücklicherweise gibt es bereits ein Projekt, dass sich um die Implementierung eines solchen MetroWindows gekümmert hat, nämlich das MahApps.Metro Projekt. Den Style von dort können wir ganz einfach an unsere Wünsche anpassen, indem wir folgende Resourcen überschreiben:

<Color x:Key="AccentColor">#2D2D30</Color>
<Color x:Key="WhiteColor">#2D2D30</Color>
<Color x:Key="BlackColor">#FFFFFFFF</Color>

Screen1Screen2

AccentColor setzt den Hintergrund der Titelleiste (mit Schließen, Minimieren … Buttons) und WhiteColor legt den Window-Hintergrund fest. Die beiden sind in unserem Fall gleich. BlackColor ist der Vordergrund für das MetroWindow.

Download

Meine Visual Studio Styles sind jetzt auch Teil des MahApps.Metro Projekts und können also vom GitHub Repository mit runtergeladen werden. Wer also den Sourcecode für das MetroWindow und die VS Styles haben möchte, kann sich das Repository hier auschecken. Die VS Styles liegen dann unter MahApps.Metro/Styles/VS, das Beispielfenster findet man unter MetroDemo (wichtig für alle StarTreck Fans: Käptn Kirk schläft immer noch). Für das MahApps.Metro Projekt hab ich zusätzlich noch eine System.Windows.Interactivity-TriggerAction gebastelt, um die Tabs zu schließen, wenn das jemanden interessiert .. im normalen Download ist die aber nicht dabei (der soll ja nur die Styles zeigen).

Ansonsten hab ich natürlich auch den Artikel und den direkten Download aktualisiert, die MahApps.Metro.dll liegt dann im lib Verzeichnis:

Direkter Download

Codeproject Artikel

Visual Studio 2012 Metro Styles für WPF–Teil 1

Obwohl viele bei dem Wort “Metro” wahrscheinlich an Windows 8 und an das “Kachelwand”-Startmenü denken werden, folgt auch der Visual Studio 2012 Style dem Metro-Prinzip.

Weil sich dieser VS-Metro-Style für komplizierte Anwendungen, die nicht unbedingt dafür gedacht sind auf irgendwelchen Smartphones oder so zu laufen, viel besser eignet, habe ich ein paar Styles für Standard Controls und eine Beispielanwendung dazu gebastelt.

Screenshots

Screen1Screen2

Übersicht

Die Styles sind in mehrere ResourceDictionaries aufgeteilt. Für jedes Control gibt es ein eigenes Dictionary. Die Styles sind jeweils mit einem Schlüssel versehen, sodass man sie explizit referenzieren muss. Das “Styles.xaml” ResourceDictionary fasst alle Styles zusammen und setzt sie als Standard (ohne Key).

Komplettes Styleset verwenden

App.xaml:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/Styles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

 

Nur Teile davon

App.xaml:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary 
                Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/ButtonStyles.xaml"/>
            <ResourceDictionary 
                Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/TextBoxStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

 

Bei Verwendung explizit referenzieren:

<Button Content="blastallenemies.cmd" Style="{StaticResource LinkButton}"/>

 

Liste der Styles (Typ und Key)

TargetType Key
Button StandardButton
Button LinkButton
TabControl StandardTabControl
Menu StandardMenu
ListBox StandardListBox
ScrollBar StandardScrollBar
TextBox StandardTextBox
TextBox SearchTextBox

Download

Direkter Download

Codeproject Artikel

Neue Version vom Rehosted Workflow Designer

Die neue Version von meinem dynamischen Rehosted Workflow Designer steht auf codeproject zum Download. Die wichtigsten Neuerungen sind neben einigen Bug Fixes und Refraktorisierungen vor allem UI Verbesserungen.

Select Workflow Type Dialog

Den Dialog zur Typauswahl habe ich komplett überarbeitet. Der Style ist dunkler geworden und die ListBox an der linken Seite hat ein schickes TreeView ersetzt. Damit ist es jetzt möglich die Workflowtypen in Gruppen einzuteilen und so anzuzeigen. IWorkflowType leitet dazu von dem Interface IAppTreeDataProvider ab, welches relevante Daten für das TreeView bereitstellt.

public interface IAppTreeDataProvider
{
     string Name { get; }
     string Path { get; }
     // ..
}

Fehleranzeige

Die Fehleranzeige für ungültiges Xaml habe ich überarbeitet. Neben der Anzeige von Zeilennummern in der TextBox kann sich der Balken zum Anzeigen der Exceptions an längere Texte anpassen und in der Größe variieren.

Download: Hier nochmal der Link zum codeproject Artikel, oder hier der direkte Download von meinem FTP  Server.

WPF rätselhaft – Animation und EventTrigger im Control Template

Heute etwas ganz komisches mit meiner Anwendung passiert. Beim Schließen von meinen Tabs verwende ich Buttons mit Schließen-Kreuzen(Path). Könnte ja alles schön einfach sein, zum Beispiel so:


<Button Width="25" Height="25">
<Canvas Height="10" Width="10" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Name="Path" StrokeThickness="0">
<Path.Fill>
<SolidColorBrush Color="#20000000"/>
</Path.Fill>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" IsClosed="True" IsFilled="True">
<LineSegment Point="3,0"/>
<LineSegment Point="5,4"/>
<LineSegment Point="7,0"/>
<LineSegment Point="10,0"/>
<LineSegment Point="6,5"/>
<LineSegment Point="10,10"/>
<LineSegment Point="7,10"/>
<LineSegment Point="5,6"/>
<LineSegment Point="3,10"/>
<LineSegment Point="0,10"/>
<LineSegment Point="4,5"/>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
<Button.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Path" From="#20000000" To="Red" Duration="0:0:0:0.2"
Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Path" From="Red" To="#20000000" Duration="0:0:0:0.2"
Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>

Dann noch einen schönen kleinen Style, damit man den Hintergrund nicht sieht, alles kein Thema…

<Style TargetType="{x:Type Button}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Rectangle Fill="Transparent"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Blöd nur, dass zwar alles funktioniert, aber beim Schließen vom Tab eine Exception geschmissen wird, die irgendwie nicht erwartet ist: „‚Path‘ name cannot be found in the name scope of ‚System.Windows.Controls.Button‘.“

Hä? Da steht doch Path beim Name! Komische Sache, liegt wahrscheinlich irgendwie da dran, dass der Button zugemacht wird nehm ich an. Also mal einen Versuch die Event Trigger für den MouseOver Effekt auf das Control Template zu packen(was ja irgendwie noch da wäre dann..). Funktioniert!

Hier noch mal der korrigierte Quellcode, steht jetzt alles im Style, lässt sich aber eben nicht vermeiden:

für den Button

<Button Width="25" Height="25"/>

und für den Style

<Style TargetType="{x:Type Button}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Rectangle Fill="Transparent"/>
<Canvas Height="10" Width="10" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Name="Path" StrokeThickness="0">
<Path.Fill>
<SolidColorBrush Color="#20000000"/>
</Path.Fill>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" IsClosed="True" IsFilled="True">
<LineSegment Point="3,0"/>
<LineSegment Point="5,4"/>
<LineSegment Point="7,0"/>
<LineSegment Point="10,0"/>
<LineSegment Point="6,5"/>
<LineSegment Point="10,10"/>
<LineSegment Point="7,10"/>
<LineSegment Point="5,6"/>
<LineSegment Point="3,10"/>
<LineSegment Point="0,10"/>
<LineSegment Point="4,5"/>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Path" From="#20000000" To="Red" Duration="0:0:0:0.2" Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Path" From="Red" To="#20000000" Duration="0:0:0:0.2" Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Find ich schon ein bisschen seltsam, aber schön es zu wissen. Ich hoffe irgendjemand der das gleiche Problem hat liest das hier.

<Button Width="25" Height="25">
    <Canvas Height="10" Width="10" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Path Name="Path" StrokeThickness="0">
            <Path.Fill>
                <SolidColorBrush Color="#20000000"/>
            </Path.Fill>
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigure StartPoint="0,0" IsClosed="True" IsFilled="True">
                            <LineSegment Point="3,0"/>
                            <LineSegment Point="5,4"/>
                            <LineSegment Point="7,0"/>
                            <LineSegment Point="10,0"/>
                            <LineSegment Point="6,5"/>
                            <LineSegment Point="10,10"/>
                            <LineSegment Point="7,10"/>
                            <LineSegment Point="5,6"/>
                            <LineSegment Point="3,10"/>
                            <LineSegment Point="0,10"/>
                            <LineSegment Point="4,5"/>
                        </PathFigure>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>
    <Button.Triggers>
        <EventTrigger RoutedEvent="MouseEnter">
            <BeginStoryboard>
                <Storyboard>
                    <ColorAnimation Storyboard.TargetName="Path" From="#20000000" To="Red" Duration="0:0:0:0.2"
                        Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger RoutedEvent="MouseLeave">
            <BeginStoryboard>
                <Storyboard>
                    <ColorAnimation Storyboard.TargetName="Path" From="Red" To="#20000000" Duration="0:0:0:0.2"
                        Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Button.Triggers>
</Button>

Events als Commands ins ViewModel durchleiten

Es ist ein relativ bekanntes Problem, die von manchen WPF Controls ausgelösten Events in Commands umzuwandeln, die ans View Model gebunden werden können. Wenn man zum Beispiel ein KeyDown Ereignis behandeln will muss man normalerweise das normalerweise immer in der Code-Behind Datei tun, es sei denn man überlegt sich eine Methode, wie man das Event in einen Command gewandelt bekommt. Ein Ansatz für solche Umwandlungen eröffnet sich mit der System.Windows.Interactivity.dll, die uns erlaubt sogenannte Behaviors zu definieren. Interessant ist hier die TriggerAction Klasse, die als Reaktion auf Events ausgeführt werden kann. Die Implementierung sieht dann so aus:

public class CommandExecuteAction : TriggerAction<DependencyObject>
{
    protected override void Invoke(object parameter)
    {
        if (this.Command != null)
        {
            if (this.Command.CanExecute(parameter))
            {
                this.Command.Execute(parameter);
            }
        }
    }

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandExecuteAction));
}

Und der Xaml – Code, der aus einem Event einen Command macht(Beispielsweise für das Hyperlink_RequestNavigate Event), wobei i = System.Windows.Interactivity und tAct mein eigener namespace mit der Trigger Action ist:

<Hyperlink NavigateUri="{Binding Url}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="RequestNavigate">
            <tAct:CommandExecuteAction Command="{Binding RequestNavigateCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TextBlock Text="{Binding Title}" TextWrapping="Wrap"/>
</Hyperlink>

How to implement a paint control in WPF

Fertiges PaintControl
Fertiges PaintControl

Wenn man den Pattern Recognizer mal genauer unter die Lupe nimmt stellt man fest, dass die Pixel durch einfache Check-Boxes dargestellt werden, die ledeglich ein bisschen anderes Aussehen verpasst bekommen haben. Das mag in dem Fall gehen, ist generell aber nicht so toll, weil man jedes Pixel einzeln anklicken muss. Auch wollte ich bei meiner RBM nicht noch mal so was machen, und hatte auch keine Lust eine riesen XAML Datei mit einem Grid mit 28 und mehr Spalten und Zeilen zu verwenden. Also muss ein Steuerelement her, was einfach so benutzt werden kann, mit einer Codezeile, Weite und Höhe angeben… Deshalb soll es jetzt mal darum gehen ein Control zu entwickeln, in dem man Zahlen, Formeln, … also Muster eben, einzeichnen kann, so ähnlich wie in Paint. Das von .NET mitgelieferte InkCanvas scheidet hier aus dem ganz einfachen Grund aus, dass es die Muster als Striche erkennt, die  neuronalen Netze brauchen zur Analyse aber die Pixel-Daten.

Also brauchen wir zuerst ein einfaches UserControl, das  PaintControl. Das bekommt jetzt als Content einfach ein durchsichtiges Rechteck, damit das was später noch draufgemalt werden soll auch schön sichtbar bleibt. Kein Content geht nicht, weil sonst an den leeren Stellen keine Click-Events und derartiges greifen.

Dann überschreiben wir in der  Code – Behind Datei die Methode OnRender, hier werden dann später die  einzelnen Punkte des Musters gezeichnet.

Jetzt sind noch ein paar Dependency Properties nötig, nämlich die Anzahl der logischen Pixel(Weite und Höhe), die nicht unbedingt mit den absoluten übereinstimmen müssen. Das heißt ein Punkt in meinem  Muster kann mehreren Pixeln in Wirklichkeit entsprechen. Als letztes brauchen wir noch die Pixel – Daten als bool[,] mit Index Weite und Höhe, und natürlich den Handler für das Mouse Move Event, weil ja gezeichnet werden soll, wenn der beutzer die Maus bewegt.

Hier nochmal alle Vorbereitungen auf einen Blick:

XAML Code von PaintControl.xaml(sehr simpel):

<UserControl x:Class="RBM.UIExtentions.PaintControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" d:DesignHeight="400" d:DesignWidth="400" MouseMove="PaintControl_OnPaint" MouseDown="PaintControl_OnPaint">
    <Rectangle Fill="Transparent"/>
</UserControl>
using
System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace UIExtentions
{
    /// <summary>
    /// Interaction logic for PaintControl.xaml
    /// </summary>
    public partial class PaintControl : UserControl
    {
        public PaintControl()
        {
            InitializeComponent();
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
        }

        public int PixelWidth
        {
            get { return (int)GetValue(PixelWidthProperty); }
            set { SetValue(PixelWidthProperty, value); }
        }

        public static readonly DependencyProperty PixelWidthProperty =
            DependencyProperty.Register("PixelWidth", typeof(int), typeof(PaintControl));

        public int PixelHeight
        {
            get { return (int)GetValue(PixelHeightProperty); }
            set { SetValue(PixelHeightProperty, value); }
        }

        public static readonly DependencyProperty PixelHeightProperty =
            DependencyProperty.Register("PixelHeight", typeof(int), typeof(PaintControl));

        public bool[,] Data
        {
            get { return (bool[,])GetValue(DataProperty); }
            private set { this.SetValue(DataPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey DataPropertyKey =
            DependencyProperty.RegisterReadOnly("Data", typeof(bool[,]), typeof(PaintControl), new PropertyMetadata());

        public static readonly DependencyProperty DataProperty = DataPropertyKey.DependencyProperty;

        public void Clear()
        {
            this.Data = new bool[this.PixelWidth, this.PixelHeight];
            this.InvalidateVisual();
        }

        private void PaintControl_MouseMove(object sender, MouseEventArgs e)
        {
        }
    }
}

C# Code von PaintControl.xaml.cs

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace RBM.UIExtentions
{
    /// <summary>
    /// Interaction logic for PaintControl.xaml
    /// </summary>
    public partial class PaintControl : UserControl
    {
        public PaintControl()
        {
            InitializeComponent();
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
        }

        public int PixelWidth
        {
            get { return (int)GetValue(PixelWidthProperty); }
            set { SetValue(PixelWidthProperty, value); }
        }

        public static readonly DependencyProperty PixelWidthProperty =
            DependencyProperty.Register("PixelWidth", typeof(int), typeof(PaintControl));

        public int PixelHeight
        {
            get { return (int)GetValue(PixelHeightProperty); }
            set { SetValue(PixelHeightProperty, value); }
        }

        public static readonly DependencyProperty PixelHeightProperty =
            DependencyProperty.Register("PixelHeight", typeof(int), typeof(PaintControl));

        public bool[,] Data
        {
            get { return (bool[,])GetValue(DataProperty); }
            private set { this.SetValue(DataPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey DataPropertyKey =
            DependencyProperty.RegisterReadOnly("Data", typeof(bool[,]), typeof(PaintControl), new PropertyMetadata());

        public static readonly DependencyProperty DataProperty = DataPropertyKey.DependencyProperty;

        // Methode zum Zurücksetzen der Pixel Daten, manchmal ganz nützlich
        public void Clear()
        {
            this.Data = new bool[this.PixelWidth, this.PixelHeight];
            this.InvalidateVisual();
        }

        private void PaintControl_OnPaint(object sender, MouseEventArgs e)
        {
        }
    }
}

Die OnPaint Methode wird immer aufgerufen, wenn an der aktuellen Mausposition gezeichnet werden soll, vorrausgesetzt die Maustaste ist gedrückt, was dann noch überüprüft werden muss. Die Implementierung sieht also so aus(mit einer kleinen Hilfsmethode zum Umrechnen von absoluten und logischen Pixeln)

private void PaintControl_OnPaint(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        Point logicalCoord = this.GetLogicalCoordinates(e.GetPosition(this));
        if (logicalCoord.X < this.PixelWidth && logicalCoord.Y < this.PixelHeight)
        {
            this.Data[(int)logicalCoord.X, (int)logicalCoord.Y] = true;
            this.InvalidateVisual();
        }
    }
}

private Point GetLogicalCoordinates(Point mousePosition)
{
    return new Point(mousePosition.X / (this.ActualWidth / this.PixelWidth), mousePosition.Y / (this.ActualHeight / this.PixelHeight));
}

Jetzt noch in der Render Methode die Pixel hinzeichnen:

protected override void OnRender(DrawingContext drawingContext)
{
    if (this.Data == null || this.Data.Length != this.PixelWidth * this.PixelHeight)
    {
        this.Data = new bool[this.PixelWidth, this.PixelHeight];
    }

    SolidColorBrush brush = new SolidColorBrush(Colors.Black);

    for (int y = 0; y < this.PixelHeight; y++)
    {
        for (int x = 0; x < this.PixelWidth; x++)
        {
            if (this.Data[x, y])
            {
                Rect rect = new Rect(((double)x / (double)this.PixelWidth) * this.ActualWidth, ((double)y / (double)this.PixelHeight) * this.ActualHeight,
                    this.ActualWidth / this.PixelWidth, this.ActualHeight / this.PixelHeight);

                GuidelineSet guidelines = new GuidelineSet();
                guidelines.GuidelinesX.Add(rect.Left);
                guidelines.GuidelinesX.Add(rect.Right);
                guidelines.GuidelinesY.Add(rect.Top);
                guidelines.GuidelinesY.Add(rect.Bottom);

                drawingContext.PushGuidelineSet(guidelines);
                drawingContext.DrawRectangle(brush, null, rect);
                drawingContext.Pop();
            }
        }
    }
}

OK, das wars das Paint Control ist soweit fertig, ich hab es ja im vorigen Artikel auch schon eingesetzt.