[UWP] Bilder bemalen mit dem InkCanvas

Mit der Universal Windows Plattform unter Windows 10 wurde eine neue Ink-API eingeführt. Das InkCanvas-Control ermöglicht es, mit der Maus, durch Touch-Eingabe oder mit einen speziellen Stift Zeichnungen zu erstellen, beispielsweise um bestimmte Bereiche in einem Bild zu markieren. Die Art und Weise wie die gemalten Striche als Bitmap gespeichert werden können hat sich im Vergleich zur Windows 8.1 Version des IncCanvas-Controls geändert.

Unter 8.1 konnten die gezeichneten Striche mit Hilfe der RenderTargetBitmap-Klasse in ein Bitmap-Objekt gerendert werden. Unter UWP werden die gezeichneten Linien allerdings nicht mehr im RendertargetBitmap dargestellt (siehe Stackoverflow link). Dafür gibt es in der UWP-API nun die InkStrokeContainer-Klasse, die alle gezeichneten Striche (Strokes) sammelt und die SaveAsync(…)-Methode bereitstellt, mit der die Striche in einen Stream exportiert werden können. Aus diesen Stream kann dann ein Bitmap erstellt werden, das die Striche repräsentiert.

Dieses Vorgehen möchte ich anhand eines kleinen Beispiels zeigen. Um das Beispiel einfach zu halten befindet sich sämtlicher Code in der Code-Behind-Datei folgender Seite:

<Page ...>       
      ...
        <ScrollViewer IsHitTestVisible="True" 
                      ManipulationMode="All"
                      HorizontalScrollBarVisibility="Visible">
            <Grid>
                <Image x:Name="TheImage"
                       Stretch="None"/>
                <InkCanvas x:Name="TheInkCanvas" />
            </Grid>
        </ScrollViewer>
    ...
</Page>

Die Seite besteht aus einem Image-Control (TheImage) das ein geladenes Bild anzeigt und den darüber liegenden InkCanvas-Control (TheInkCanvas), das verwendet wird, um Markierungen im Bild einzufügen. Das InkCanvas-Control besitzt eine Instanz der InkPresenter-Klasse, anhand derer sämtliche Zeichen-Einstellungen vorgenommen werden können. Der InkPresenter beinhaltet wiederum den genannten InkStrokeContainer. Um die Striche als Bitmap zu speichern reicht es aus, die SaveAsync(…)-Methode aufzurufen und aus dem Stream ein neues Bitmap zu erstellen.

            InkStrokeContainer container = TheInkCanvas.InkPresenter.StrokeContainer;
            ...
            // Striche in WriteableBitmap speichern
            WriteableBitmap bmp;
            using (InMemoryRandomAccessStream ims =
                new InMemoryRandomAccessStream())
            {
                await container.SaveAsync(ims);
                bmp = await new WriteableBitmap(1, 1)
                    .FromStream(ims, BitmapPixelFormat.Bgra8);
            }

Doch es gibt ein Problem:

Die Dimension des so entstehenden Bitmaps entspricht nicht der des geladenen Bildes, sondern der des minimalen umfassenden Rechtecks der gezeichneten Striche (siehe Bild). Um die Striche und das geladene Bild im Anschluss kombinieren zu können, sollten beide Bilder die gleiche Dimension besitzen. Der gelbe Rahmen im folgenden Bild veranschaulicht die Dimension des entstehenden Bildes:

lena_modified_bounding Das Problem kann gelöst werden, in dem das umfassende Rechteck der Strokes auf die Größe des geladenen Bildes vergrößert wird. Das kann durch einen kleinen Trick erreicht werden:  Mit unsichtbarer Tinte (Strichstärke kleiner 1 px) werden oben links und unten rechts kurze Striche per Code eingezeichnet. Die Ecke unten rechts wird von der Dimension des Bildes bestimmt, in die die Striche eingezeichnet werden sollen. Zum Schluss wird das so entstandene Bild mit dem geladenen Bild zusammengefügt. Im Beispiel geschieht dies durch die WriteableBitmap.Blit(…)-Methode. Diese und einige weitere hilfreiche Methoden für den Umgang mit Bitmaps sind in der WritableBitmapEx-Bibliothek definiert (WriteableBitmapEx Nuget).

 

        private async Task SaveStrokesToBitmap(WriteableBitmap b)
        {
            Rect imgRect = new Rect(0, 0, b.PixelWidth, b.PixelHeight);
            InkStrokeContainer container = TheInkCanvas.InkPresenter.StrokeContainer;
            InkStrokeBuilder builder = new InkStrokeBuilder();

            // Unsichtbare Tinte!
            InkDrawingAttributes da = TheInkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
            da.Size = new Size(0.1, 0.1);
            builder.SetDefaultDrawingAttributes(da);

            // Strich in oberer linker Ecke einfügen
            InkStroke topLeft = builder.CreateStroke(new List<Point>() {
                new Point(1, 1),
                new Point(2, 2) });
            container.AddStroke(topLeft);

            // Strich in unterer Rechter Ecke einfügen
            InkStroke bottomRight = builder.CreateStroke(new List<Point>() {
                new Point(imgRect.Width -2, imgRect.Height -2),
                new Point(imgRect.Width -1, imgRect.Height -1) });   
            container.AddStroke(bottomRight);

            // Striche in WriteableBitmap speichern
            WriteableBitmap bmp;
            using (InMemoryRandomAccessStream ims =
                new InMemoryRandomAccessStream())
            {
                await container.SaveAsync(ims);
                bmp = await new WriteableBitmap(1, 1)
                    .FromStream(ims, BitmapPixelFormat.Bgra8);
            }
            // Bilder zusammenfügen
            b.Blit(imgRect, bmp, imgRect, WriteableBitmapExtensions.BlendMode.Alpha);
        }

Nach den Aufruf von SaveStrokeToBitmap(…) enthält das übergebene WriteableBitmap-Objekt die gezeichneten Striche des InkCanvas.

Das Beispielprojekt findet ihr unter https://github.com/robibobi/blog-IncCanvas

screenshot_sample_project

Leave a Comment

Your email address will not be published. Required fields are marked *