I have a control that throws events and I need to draw immediately after said events fire. The event fires around 15 times per second with a regular interval, and are handled as expected. I tried the following scenarios:
Scenario 1
In XAML, I created a canvas. Whenever events from custom control fire, I update a counter. When the CompositionTarget event fires and the counter has changed, the canvas gets redrawn (based on the counter). However, this is too slow. The CompositionTarget event fires at a low speed and is irregular. See:
Why is Frame Rate in WPF Irregular and Not Limited To Monitor Refresh?
Scenario 2
I installed the SkiaSharp WPF Nuget packages. Tried basically the same thing as in Scenario 1, this time using the OnPaintSurface event. But I get more-or-less the same results, it seems that OnPaintService is called in a similar way as CompositionTarget.
Demo (notice the irregular updates, sometimes frames hanging or dropping):
Code from demo:
<Window x:Class="TMCVisualizer.AlphaWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TMCVisualizer" xmlns:skia="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF" xmlns:skiasharp="clr-namespace:SkiaSharp;assembly=SkiaSharp" mc:Ignorable="d"
Title="AlphaWindow" Width="1000" Height="480" WindowStyle="SingleBorderWindow" AllowsTransparency="False" ContentRendered="Handle_ContentRendered">
<Grid Background="Beige" x:Name="grid">
<skia:SKElement PaintSurface="OnPaintSurface" IgnorePixelScaling="True" Height="210" VerticalAlignment="Bottom" x:Name="board"></skia:SKElement>
</Grid>
</Window>
//Removed attaching event handlers and such for clarity
private float _index = -1;
private void OnCustomControlEvent(float counter, DateTime dateTime)
{
_index = counter;
this.Dispatcher.Invoke(() =>
{
board.InvalidateVisual();
//Will trigger redraw => OnPaintSurface
});
}
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
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(_index.ToString(), coord, paint);
}
Also, I tried to directly draw on a writeable bitmap using SkiaSharp, again with the same results. The code I wrote looks similar to this:
The most efficient way to draw in SkiaSharp without using PaintSurface event
Is there any way to draw directly in WPF? Maybe I'm missing something, or maybe I don't fully understand SkiaSharp yet? Is there an alternative package I can use, such as SkiaSharp? I'd hate to say goodbye to WPF, because there are some other WPF components that I need to use in my app.
Edit : What I am trying to achieve:
My "component" that fires the events is a mod music player (SharpMik). I want to draw the notes that are being played in the player on screen (like a music tracker). When using the OnPaintSurface and a regular canvas, I couldn't get a better result than this (when the music is playing at the same time you see that the notes are not properly updated with the beat (slight delays)):
Code for the above result:
<Window x:Class="TMCVisualizer.AlphaWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TMCVisualizer" mc:Ignorable="d"
Title="AlphaWindow" Width="1000" Height="480" WindowStyle="SingleBorderWindow" AllowsTransparency="False" ContentRendered="Handle_ContentRendered">
<Canvas x:Name="canvas" Height="210" VerticalAlignment="Bottom" />
</Window>
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using SharpMik;
using SharpMik.Drivers;
using SharpMik.Player;
using SharpMik.Player.Events.EventArgs;
namespace TMCVisualizer
{
public partial class AlphaWindow : Window
{
public Module ModModule;
public MikMod ModPlayer;
private Dictionary<int, List<TextBlock>> _dict = new Dictionary<int, List<TextBlock>>();
public List<Row> Rows;
private string _track = @"C:\Users\Wroah\Documents\MEKX-RMB.S3M";
private float _index = -1F;
public AlphaWindow()
{
InitializeComponent();
((App)Application.Current).WindowPlace.Register(this);
this.Loaded += Handle_AlphaWindow_Loaded;
}
private void Handle_AlphaWindow_Loaded(object sender, RoutedEventArgs e)
{
ModPlayer = new MikMod();
ModPlayer.Init<NaudioDriver>("");
}
private void SetupAndPlay()
{
CompositionTarget.Rendering -= Handle_Tick;
ModModule = ModPlayer.LoadModule(_track);
Rows = ModPlayer.Export(ModModule);
DrawGrid();
//Load again
ModModule = ModPlayer.LoadModule(_track);
this.KeyUp += Handle_AlphaWindow_KeyUp;
CompositionTarget.Rendering += Handle_Tick;
}
private void Handle_AlphaWindow_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
Task.Run(() =>
{
ModPlayer.Play(ModModule);
});
}
}
private void Handle_Tick(object sender, EventArgs e)
{
var index = ModPlayer.GetCurrentIndex();
if (index != _index)
{
_index = index;
UpdateGrid();
}
}
private void Handle_ContentRendered(object sender, EventArgs e)
{
SetupAndPlay();
}
private void UpdateGrid()
{
for (var i = 0; i < 13; i++)
{
var dictIndex = (int)_index;
dictIndex -= 7;
dictIndex += i;
var hasData = dictIndex >= 0;
var list = _dict[i];
var cols = ModModule.numchn;
Row row = null;
if (hasData)
{
row = Rows[dictIndex];
}
for (var j = 0; j <= cols; j++)
{
if (j == 0)
{
//Draw pattern position counter
if (hasData)
list[0].Text = row.Patpos.ToString("D2");
else
list[0].Text = "";
}
else
{
if (hasData)
list[j].Text = row.Cols[j - 1].note;
else
list[j].Text = ".";
}
}
}
}
private void DrawGrid()
{
var xPos = 0;
var yPos = 1;
var width = canvas.ActualWidth;
canvas.Children.Clear();
canvas.Background = Brushes.Black;
for (var i = 0; i < 13; i++)
{
var line = new Line();
line.X1 = xPos;
line.X2 = width;
line.Y1 = yPos;
line.Y2 = yPos;
line.StrokeThickness = (int)1;
line.SnapsToDevicePixels = true;
line.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
line.Stroke = Brushes.Gray;
if (i == 6)
{
var brush = new SolidColorBrush(Colors.White);
brush.Opacity = .7;
var rect = new Rectangle();
rect.Width = width;
rect.Height = 15;
rect.Fill = brush;
rect.SnapsToDevicePixels = true;
Canvas.SetLeft(rect, 0);
Canvas.SetTop(rect, yPos);
canvas.Children.Add(rect);
}
canvas.Children.Add(line);
var list = new List<TextBlock>();
for (var j = 0; j < 64; j++)
{
var txt = new TextBlock();
txt.FontFamily = new FontFamily("Consolas");
txt.Width = 30;
txt.Foreground = Brushes.White;
if (i == 6)
txt.Foreground = Brushes.Black;
txt.TextAlignment = TextAlignment.Center;
txt.SnapsToDevicePixels = true;
txt.IsEnabled = false;
Canvas.SetTop(txt, yPos);
Canvas.SetLeft(txt, j * 30);
canvas.Children.Add(txt);
list.Add(txt);
}
_dict.Add(i, list);
yPos += 15;
}
}
}
}