The most efficient way to draw in SkiaSharp without using PaintSurface event - wpf

An extension of this question. I'm using SkiaSharp to draw shapes with OpenGL. Most of code samples for WPF suggest using SKElement control and get reference to the drawing surface inside PaintSurface event. I need reference to drawing surface al the time, not only inside of this event, so I'm using SKBitmap and SKCanvas and send the output to Image control in WPF.
XAML
<Canvas x:Name="ScreenControl" />
CS
var bmp = new SKBitmap((int)ScreenControl.ActualWidth, (int)ScreenControl.ActualHeight);
var canvas = new SKCanvas(bmp);
var image = new Image
{
Stretch = Stretch.Fill
};
canvas.DrawText("Demo", new SKPoint(20, 20), new SKPaint { TextSize = 20 });
var index = 0;
var clock = new Timer();
var wrt = bmp.ToWriteableBitmap();
image.Source = wrt;
clock.Interval = 100;
clock.Elapsed += (object sender, ElapsedEventArgs e) =>
{
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
var wrt = bmp.ToWriteableBitmap(); // Potentially slow line #1
canvas.Clear();
canvas.DrawText((index++).ToString(), new SKPoint(20, 20), new SKPaint { TextSize = 20 });
image.Source = wrt; // Potentially slow line #2
image.InvalidateVisual();
}));
};
clock.Start();
ScreenControl.Children.Clear();
ScreenControl.Children.Add(image);
Project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp.Views.WPF" Version="2.80.3-preview.18" />
</ItemGroup>
</Project>
Question
Is there a way to draw faster, specifically, without recreating bitmap bmp.ToWriteableBitmap() and without changing image source every time image.Source = wrt? Back buffer or something related?

You have to use Skia's SKElement:
<Grid>
<skia:SKElement PaintSurface="OnPaintSurface" IgnorePixelScaling="True" />
</Grid>
And draw in OnPaintSurface:
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
// the the canvas and properties
var canvas = e.Surface.Canvas;
// make sure the canvas is blank
canvas.Clear(SKColors.White);
// draw some text
var paint = new SKPaint
{
Color = SKColors.Black,
IsAntialias = true,
Style = SKPaintStyle.Fill,
TextAlign = SKTextAlign.Center,
TextSize = 24
};
var coord = new SKPoint(e.Info.Width / 2, (e.Info.Height + paint.TextSize) / 2);
canvas.DrawText("SkiaSharp", coord, paint);
}

Related

Add event handler (MouseDown) dynamically for PathFigure C# WPF

I created an object from points with this code, dynamically:
SolidColorBrush brushColor = (SolidColorBrush)new BrushConverter().ConvertFromString(_brushColor);
PathFigure figures = new PathFigure();
figures.StartPoint = points[0];
points.RemoveAt(0);
figures.Segments = new PathSegmentCollection(points.Select((p, i) => new LineSegment(p, i % 2 == 0)));
PathGeometry pg = new PathGeometry();
pg.Figures.Add(figures);
canvas.Children.Add(new Path { Stroke = brushColor, StrokeThickness = 3, Data = pg });
Now I want to add event handler for this object. Would not be a problem if object is a path or polyline type. I would just add event handler like this:
poly.MouseDown += new MouseButtonEventHandler(poly_MouseDown);
void poly_MouseDown(object sender, MouseButtonEventArgs e)
{
//code
}
Problem is that I have to use Figures and PathGeometry which does not accept MouseDown event handlers. Since they are from System.Windows.Media class and Path/Polyline is from System.Windows.Shapes I can't find a solution to assign right event handler (MouseDown) to my Figure object.
What is the solution or is there any nice workaround solution for this problem? Maybe cast or convert it somehow?
As I see it you're skipping a crucial step.
Declare the Path Object first, give it the event, and then insert it into your Canvas:
SolidColorBrush brushColor = (SolidColorBrush)new BrushConverter ().ConvertFromString (_brushColor);
PathFigure figures = new PathFigure ();
figures.StartPoint = points[0];
points.RemoveAt (0);
figures.Segments = new PathSegmentCollection (points.Select ((p, i) => new LineSegment (p, i % 2 == 0)));
PathGeometry pg = new PathGeometry ();
pg.Figures.Add (figures);
Path pgObject = new Path({ Stroke = brushColor, StrokeThickness = 3, Data = pg });
pgObject.MouseDown+=new MouseButtonEventHandler(poly_MouseDown);
canvas.Children.Add (pgObject);

Add TransformGroup to a FramworkElement when rendering WPF to a PNG

I've got an app that turns some XAML Usercontrols into PNGs - this has worked really well up to now, unfortunately I now need to double the size of the images.
My method (that doesn't work!) was to add a ScaleTransform to the visual element after I've loaded it ...
This line is the new line at the top of the SaveUsingEncoder method.
visual.RenderTransform = GetScaleTransform(2);
The PNG is the new size (3000 x 2000) - but the XAML is Rendered at 1500x1000 in the centre of the image.
Can anyone assist please?
private void Load(string filename)
{
var stream = new FileStream(filename), FileMode.Open);
var frameworkElement = (FrameworkElement)(XamlReader.Load(stream));
var scale = 2;
var encoder = new PngBitmapEncoder();
var availableSize = new Size(1500 * scale, 1000 * scale);
frameworkElement.Measure(availableSize);
frameworkElement.Arrange(new Rect(availableSize));
name = name.Replace(" ", "-");
SaveUsingEncoder(frameworkElement, string.Format(#"{0}.png", name), encoder, availableSize);
}
private TransformGroup GetScaleTransform(int scale)
{
var myScaleTransform = new ScaleTransform {ScaleY = scale, ScaleX = scale};
var myTransformGroup = new TransformGroup();
myTransformGroup.Children.Add(myScaleTransform);
return myTransformGroup;
}
private void SaveUsingEncoder(FrameworkElement visual, string fileName, BitmapEncoder encoder, Size size)
{
visual.RenderTransform = GetScaleTransform(2);
var bitmap = new RenderTargetBitmap(
(int) size.Width,
(int) size.Height,
96,
96,
PixelFormats.Pbgra32);
bitmap.Render(visual);
var frame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(frame);
using (var stream = File.Create(fileName))
{
encoder.Save(stream);
}
}
Called visual.UpdateLayout before rendering into the RenderTargetBitmap
(Thanks to Clemens for this answer - but he put it as a comment!)

Convert WPF Path to Bitmap File

I want to be able to load a WPF Resource Dictionary of Paths and output them one by one to files (jpg, bmp, it doesn't matter). This will be in a class library that will be accessed by an MVC application to render to a http stream, so I'm doing this purely within code (no XAML pages).
I've been able to load the dictionary and iterate through the paths, but when I save the images to disk, they are blank. I know I'm missing something trivial, like applying the path to a piece of geometry, or adding it into some containing rectangle or something, but my WPF experience is somewhat limited.
I'm using the following code:
I have a WPF Resource Dictionary containing several paths, such as the following:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Path x:Key="Path1" Data="M 100,200 C 100,25 400,350 400,175 H 280" Fill="White" Margin="10,10,10,10" Stretch="Fill"/>
<Path x:Key="Path2" Data="M 10,50 L 200,70" Fill="White" Margin="10,10,10,10" Stretch="Fill"/>
</ResourceDictionary>
And the class to read and output the files:
public class XamlRenderer
{
public void RenderToDisk()
{
ResourceDictionary resource = null;
Thread t = new Thread(delegate()
{
var s = new FileStream(#"C:\Temp\myfile.xaml", FileMode.Open);
resource = (ResourceDictionary)XamlReader.Load(s);
s.Close();
foreach (var item in resource)
{
var resourceItem = (DictionaryEntry)item;
var path = (System.Windows.Shapes.Path)resourceItem.Value;
var panel = new StackPanel();
var greenBrush = new SolidColorBrush {Color = Colors.Green};
path.Stroke = Brushes.Blue;
path.StrokeThickness = 2;
path.Fill = greenBrush;
panel.Children.Add(path);
panel.UpdateLayout();
string filepath = #"C:\Temp\Images\" + resourceItem.Key + ".jpg";
SaveImage(panel, 64, 64, filepath);
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
public void SaveImage(Visual visual, int width, int height, string filePath)
{
var bitmap =
new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(visual);
var image = new PngBitmapEncoder();
image.Frames.Add(BitmapFrame.Create(bitmap));
using (Stream fs = File.Create(filePath))
{
image.Save(fs);
}
}
}
After much googling, and trial and error, I seem to have arrived at a solution. This post pointed me in the right direction.
There were a few issues:
I was now setting the size of the path and the container
The stack panel container has some nuances that were causing issues, so I replaced it with a canvas
Most importantly, Measure() and Arrange() need to be called on the container element. The UpdateLayout() call is not needed.
Once these issues were fixed, the images rendered to disk (although there's an aspect ratio problem I have yet to fix).
Here is the updated code:
public void RenderToDisk()
{
ResourceDictionary resource = null;
Thread t = new Thread(delegate()
{
var s = new FileStream(#"C:\Temp\myfile.xaml", FileMode.Open);
resource = (ResourceDictionary)XamlReader.Load(s);
s.Close();
foreach (var item in resource)
{
var resourceItem = (DictionaryEntry)item;
var path = (System.Windows.Shapes.Path)resourceItem.Value;
path.Margin = new Thickness(10);
path.HorizontalAlignment = HorizontalAlignment.Center;
path.VerticalAlignment = VerticalAlignment.Center;
path.Width = 48;
path.Height = 48;
path.Stroke = Brushes.White;
path.Fill = Brushes.Black;
var canvas = new Canvas();
canvas.Width = 64;
canvas.Height = 64;
canvas.Margin = new Thickness(0);
canvas.Background = Brushes.Transparent;
canvas.Children.Add(path);
canvas.Measure(new Size(canvas.Width, canvas.Height));
canvas.Arrange(new Rect(new Size(canvas.Width, canvas.Height)));
string filepath = #"C:\Temp\Images\" + resourceItem.Key + ".png";
SaveImage(canvas, (int)canvas.Width, (int)canvas.Height, filepath);
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
}

Clear pixels in WPF Canvas

I have a transparent canvas on which I can draw arbitrary polylines with the mouse.
Most of the lines are semi-transparent.
Now I need some kind of an eraser tool, i.e. a polyline with an eraser brush, which allows to clear pixels along the mouse movement.
With an opaque canvas I would simply use the background brush but in this case it is Color.FromArgb(0,0,0,0) and drawing with that has no effect.
The canvas seems to be in some kind of alpha blend mode which blends anything I draw on it with what already exists, unless I set the alpha channel to 255 in which case whatever is on the canvas will be overwritten. That does not help me, as I simply want to clear the pixels, i.e. make them fully transparent.
Any ideas?
Here's the main part of the code I'm using:
public class WPFWindow : Window
{
private Canvas canvas = new Canvas();
private bool LDown = false;
private Polyline lines;
private PointCollection points;
public WPFWindow()
{
this.AllowsTransparency = true;
this.WindowStyle = WindowStyle.None;
this.Background = new SolidColorBrush( Color.FromArgb(50,0,0,0) );
this.Width = 500;
this.Height = 400;
this.Top = this.Left = 0;
canvas.Width = this.Width;
canvas.Height = this.Height;
canvas.Background = new SolidColorBrush( Color.FromArgb(0,0,0,0) );
this.Content = canvas;
this.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler( WPFWindow_MouseLeftButtonDown );
this.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler( WPFWindow_MouseLeftButtonUp );
this.MouseMove += new System.Windows.Input.MouseEventHandler( WPFWindow_MouseMove );
}
void WPFWindow_MouseMove( object sender, System.Windows.Input.MouseEventArgs e )
{
if( LDown )
{
points.Add( e.GetPosition(null) );
}
}
void WPFWindow_MouseLeftButtonUp( object sender, System.Windows.Input.MouseButtonEventArgs e )
{
LDown = false;
}
void WPFWindow_MouseLeftButtonDown( object sender, System.Windows.Input.MouseButtonEventArgs e )
{
LDown = true;
lines = new Polyline();
points = new PointCollection();
lines.Stroke = new SolidColorBrush( Color.FromArgb( 128, 180, 80, 80 ) );
lines.StrokeThickness = 20;
lines.Points = points;
points.Add( e.GetPosition(null) );
canvas.Children.Add( lines );
}
}
The WPF Canvas control is not a drawing surface, it's a "Panel", a container that arranges multiple other controls.
Each Polyline you add to the Canvas is actually a FrameworkElement (a kind of lightweight control) and they are all drawn in order (it's like adding multiple labels or edit controls, there is no way a control can change the visual representation of another control on the window except for covering it up).
You may want to create an actual image draw the polylines on the image and display that image, then you can talk about clearing pixels.
Use an InkCanvas instead of polylines. It has an eraser already implemented

WPF: Get specify image from touch

I was added picture as a children to layer called "canvas". By the following code:
if (addChild)
{
Image i = new Image();
BitmapImage src = new BitmapImage();
src.BeginInit();
src.UriSource = new Uri(path, UriKind.Absolute);
src.EndInit();
i.Source = src;
i.Width = 200;
i.IsManipulationEnabled = true;
double rotAngle = Rand.GetRandomDouble(-3.14/4, 3.14/4);
i.RenderTransform = new MatrixTransform(Math.Cos(rotAngle), -Math.Sin(rotAngle),
Math.Sin(rotAngle), Math.Cos(rotAngle), Rand.GetRandomDouble(0, this.Width - i.Width), Rand.GetRandomDouble(0, this.Height - i.Width));
canvasImages.Add(i);
canvas.Children.Add(i);
Canvas.SetZIndex(i, canvas.Children.Count-1);
addedFiles.Add(path);
maxZ++;
}
Here is the problem. I'm trying to make an event called "canvas_TouchDown" which can detect the specify picture when I touched it so that it will get the center of that image object.
List<Image> canvasImages = new List<Image>();
private void canvas_TouchDown(object sender, TouchEventArgs e)
{
foreach (Image canvasImage in canvasImages)
{
if (canvasImage.AreAnyTouchesCaptured == true)
{
System.Diagnostics.Debug.WriteLine("I found image that you touch");
}
}
}
However, there is nothing happened. I also try to use PersistId property but it doesn't work. Have any suggestion?
Regard,
C.Porawat
If you are adding the image to the canvas, touching it and expecting the canvas to receive the touch you will be disappointed. You should either listen to "touch down" on the image or "preview touch down" on the canvas.

Resources