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.