C# SkiaSharp OpenTK Winform - 如何从后台线程中绘制?
C# SkiaSharp OpenTK Winform - How to draw from a background thread?
我正在尝试用 SkiaSharp 替换 GDI+,以获得一个数据可视化框架,该框架使用实时不断变化的工程数据呈现多层可平移缩放图形。
在 GDI+ 中,应用程序是这样做的:
- 创建了一组具有透明背景的绘图层,通常是网格层、一个或多个数据层以及用于光标信息和突出显示的覆盖层,每个层都由单独的位图支持。
- 在渲染循环后台线程中,只有需要为每个渲染循环更新的层(位图)使用 GDI+ 重绘。这可能需要数以千计的计算和转换线、矩形和文本来创建热图、波形、直方图、数据标签等。
- 然后,堆栈中的每个绘图层将由后台线程通过 BitBlt 转换为复合位图
- 然后,最终的复合位图将以高达 30fps 的速度绘制到 GUI 线程中的 WinForm PictureBox。
在一个或多个后台线程中完成直到最终图像呈现的所有操作。GUI 线程仅涉及将完成的图像绘制到 PictureBox。这很重要,因为还有许多其他 GUI 控件需要保持响应。这很好用,除了它都是基于 CPU 的。小 windows 没问题,但在 4K 屏幕上最大化会减慢渲染速度,足以使程序几乎无法使用。
我想用 GPU 加速的 SkiaSharp 重新创建这个概念。
我尝试创建了几十个不同的测试程序,但我不断遇到跨线程访问冲突,或者屏幕上什么都不显示,或者硬崩溃。与其贴代码,不如问一些基本问题:
问题:
- 您将如何创建这个框架? SkiaSharp 甚至可以做到这一点吗?
- 我的每个图层 类 是否应该维护一个 SKSurface、SKCanvas、SKImage 或 SKBitmap? - 同样,如果一个图层在当前循环中不需要重绘,那么该图层需要保持之前绘制的内容,以便在下一个合成图像中使用。
- GUI 线程需要 GLControl 和 GRContext 来显示最终的合成图像,但是是否应该有另一个单独的 GRContext 供后台渲染线程使用? - 如何使用 GPU 加速创建?
- 是否有人可以指出类似概念的任何工作示例? (GPU 加速从后台线程到 GLControl 的渲染)
- 我是否应该只使用隐藏在后台的 SkiaSharp,并使用带有 PictureBox 的 GDI+ BitBlt 在屏幕上显示合成图像? - 这会解决一些线程问题吗?
任何帮助定义方法和注意事项将不胜感激!!
我想出了如何使用 SKPicture 对象来使用背景渲染线程记录来自每一层的绘制命令,然后使用 GUI 线程将它们绘制回 SKGLControl。这满足了我的所有要求:它允许多个绘图层,使用后台线程渲染,仅渲染需要更新的层,使用 GPU 加速进行绘制,并且对于最大化 4K window.
经验教训
我一路上学到的一些教训让我很困惑...
网上有使用GPU加速的OpenTK.GLControl的例子,也有使用内置GPU加速的SkiaSharp.Views.Desktop.SKGLControl的例子。 SKGLControl 绝对是这项任务的正确控制。由于 FramebufferBinding 和 StencilBits 的问题,GLControl 正在为 DrawCircle 创建正方形并拒绝渲染任何曲线?!? - 我放弃了。对于 SKPicture 对象,它也比 SKGLControl 慢。
SKGLControl 不需要也不喜欢使用 GLControl 所需的 SwapBuffers 或 Canvas.Flush。这导致了 SKGLControl 的绘图出现频闪和故障,这就是为什么我在与 GLControl 的杂草斗争中脱颖而出。当我用 SKGLControl 重建项目并摆脱 SwapBuffers 和 Canvas.Flush 时,一切开始正常运行。
对表面和画布的引用不应超过一个 PaintSurface 循环。 SKPicture 是一个神奇的对象,它可以让你存储每一层的绘图命令并一次又一次地播放它们。这与 SKBitmap 或 SKImage 不同,它们生成像素光栅而不只是记录绘制命令。我无法让 SKBitmap 或 SKImage 在多线程环境中运行并且仍然是 GPU 加速的。 SKPicture 非常适合这个。
SKGLControl 的 Paint 事件和 PaintSurface 事件是有区别的。 PaintSurface 事件是应该使用的,默认情况下是 GPU 加速的。
工作示例代码
下面是一个多层、多线程、GPU 加速的 SkiaSharp 绘图的全功能演示
此示例创建 4 个绘图层:
- 背景图层
- 网格层
- 数据层
- 叠加层
图层使用后台线程绘制(渲染),然后使用 GUI 线程绘制到 SKGLControl。每个图层只在需要时渲染,但所有图层都是用每个 PaintSurface 事件绘制的。
尝试代码:
- 在 Visual Studio 中创建一个新的 C# WinForms 项目。
- 添加 NuGet 包:“SkiaSharp.Views.WindowsForms”。这将自动添加“SkiaSharp”和“SkiaSharp.Views.Desktop.Common”。
- 向 Form1 添加 SkiaSharp.Views.Desktop.SKGLControl。将其命名为“skglControl1”
- 将 skglControl1 的 Dock 设置为“填充”,使其填充 Form1。
- 将下面的代码复制到Form1:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
namespace SkiaSharp_Multi_Layer_GPU
{
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- WinForm - Form 1 -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public partial class Form1 : Form
{
private Thread m_RenderThread = null;
private AutoResetEvent m_ThreadGate = null;
private List<Layer> m_Layers = null;
private Layer m_Layer_Background = null;
private Layer m_Layer_Grid = null;
private Layer m_Layer_Data = null;
private Layer m_Layer_Overlay = null;
private bool m_KeepSwimming = true;
private SKPoint m_MousePos = new SKPoint();
private bool m_ShowGrid = true;
private Point m_PrevMouseLoc = new Point();
// ---------------------------
// --- Form1 - Constructor ---
// ---------------------------
public Form1()
{
InitializeComponent();
}
// ------------------------------
// --- Event - Form1 - OnLoad ---
// ------------------------------
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Set the title of the Form
this.Text = "SkiaSharp Demo - Multi-Layer, Multi-Threaded, GPU Accelerated";
// Create layers to draw on, each with a dedicated SKPicture
m_Layer_Background = new Layer("Background Layer");
m_Layer_Grid = new Layer("Grid Layer");
m_Layer_Data = new Layer("Data Layer");
m_Layer_Overlay = new Layer("Overlay Layer");
// Create a collection for the drawing layers
m_Layers = new List<Layer>();
m_Layers.Add(m_Layer_Background);
m_Layers.Add(m_Layer_Grid);
m_Layers.Add(m_Layer_Data);
m_Layers.Add(m_Layer_Overlay);
// Subscribe to the Draw Events for each layer
m_Layer_Background.Draw += Layer_Background_Draw;
m_Layer_Grid.Draw += Layer_Grid_Draw;
m_Layer_Data.Draw += Layer_Data_Draw;
m_Layer_Overlay.Draw += Layer_Overlay_Draw;
// Subscribe to the SKGLControl events
skglControl1.PaintSurface += SkglControl1_PaintSurface;
skglControl1.Resize += SkglControl1_Resize;
skglControl1.MouseMove += SkglControl1_MouseMove;
skglControl1.MouseDoubleClick += SkglControl1_MouseDoubleClick;
// Create a background rendering thread
m_RenderThread = new Thread(RenderLoopMethod);
m_ThreadGate = new AutoResetEvent(false);
// Start the rendering thread
m_RenderThread.Start();
}
// ---------------------------------
// --- Event - Form1 - OnClosing ---
// ---------------------------------
protected override void OnClosing(CancelEventArgs e)
{
// Let the rendering thread terminate
m_KeepSwimming = false;
m_ThreadGate.Set();
base.OnClosing(e);
}
// --------------------------------------------
// --- Event - SkglControl1 - Paint Surface ---
// --------------------------------------------
private void SkglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
{
// Clear the Canvas
e.Surface.Canvas.Clear(SKColors.Black);
// Paint each pre-rendered layer onto the Canvas using this GUI thread
foreach (var layer in m_Layers)
{
layer.Paint(e.Surface.Canvas);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.LimeGreen;
for (int i = 0; i < m_Layers.Count; i++)
{
var layer = m_Layers[i];
var text = $"{layer.Title} - Renders = {layer.RenderCount}, Paints = {layer.PaintCount}";
var textLoc = new SKPoint(10, 10 + (i * 15));
e.Surface.Canvas.DrawText(text, textLoc, paint);
}
paint.Color = SKColors.Cyan;
e.Surface.Canvas.DrawText("Click-Drag to update bars.", new SKPoint(10, 80), paint);
e.Surface.Canvas.DrawText("Double-Click to show / hide grid.", new SKPoint(10, 95), paint);
e.Surface.Canvas.DrawText("Resize to update all.", new SKPoint(10, 110), paint);
}
}
// -------------------------------------
// --- Event - SkglControl1 - Resize ---
// -------------------------------------
private void SkglControl1_Resize(object sender, EventArgs e)
{
// Invalidate all of the Layers
foreach (var layer in m_Layers)
{
layer.Invalidate();
}
// Start a new rendering cycle to redraw all of the layers.
UpdateDrawing();
}
// -----------------------------------------
// --- Event - SkglControl1 - Mouse Move ---
// -----------------------------------------
private void SkglControl1_MouseMove(object sender, MouseEventArgs e)
{
// Save the mouse position
m_MousePos = e.Location.ToSKPoint();
// If Left-Click Drag, draw new bars
if (e.Button == MouseButtons.Left)
{
// Invalidate the Data Layer to draw a new random set of bars
m_Layer_Data.Invalidate();
}
// If Mouse Move, draw new mouse coordinates
if (e.Location != m_PrevMouseLoc)
{
// Remember the previous mouse location
m_PrevMouseLoc = e.Location;
// Invalidate the Overlay Layer to show the new mouse coordinates
m_Layer_Overlay.Invalidate();
}
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// -------------------------------------------------
// --- Event - SkglControl1 - Mouse Double Click ---
// -------------------------------------------------
private void SkglControl1_MouseDoubleClick(object sender, MouseEventArgs e)
{
// Toggle the grid visibility
m_ShowGrid = !m_ShowGrid;
// Invalidate only the Grid Layer.
m_Layer_Grid.Invalidate();
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// ----------------------
// --- Update Drawing ---
// ----------------------
public void UpdateDrawing()
{
// Unblock the rendering thread to begin a render cycle. Only the invalidated
// Layers will be re-rendered, but all will be repainted onto the SKGLControl.
m_ThreadGate.Set();
}
// --------------------------
// --- Render Loop Method ---
// --------------------------
private void RenderLoopMethod()
{
while (m_KeepSwimming)
{
// Draw any invalidated layers using this Render thread
DrawLayers();
// Invalidate the SKGLControl to run the PaintSurface event on the GUI thread
// The PaintSurface event will Paint the layer stack to the SKGLControl
skglControl1.Invalidate();
// DoEvents to ensure that the GUI has time to process
Application.DoEvents();
// Block and wait for the next rendering cycle
m_ThreadGate.WaitOne();
}
}
// -------------------
// --- Draw Layers ---
// -------------------
private void DrawLayers()
{
// Iterate through the collection of layers and raise the Draw event for each layer that is
// invalidated. Each event handler will receive a Canvas to draw on along with the Bounds for
// the Canvas, and can then draw the contents of that layer. The Draw commands are recorded and
// stored in an SKPicture for later playback to the SKGLControl. This method can be called from
// any thread.
var clippingBounds = skglControl1.ClientRectangle.ToSKRect();
foreach (var layer in m_Layers)
{
layer.Render(clippingBounds);
}
}
// -----------------------------------------
// --- Event - Layer - Background - Draw ---
// -----------------------------------------
private void Layer_Background_Draw(object sender, EventArgs_Draw e)
{
// Create a diagonal gradient fill from Blue to Black to use as the background
var topLeft = new SKPoint(e.Bounds.Left, e.Bounds.Top);
var bottomRight = new SKPoint(e.Bounds.Right, e.Bounds.Bottom);
var gradColors = new SKColor[2] { SKColors.DarkBlue, SKColors.Black };
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Shader = shader;
paint.Style = SKPaintStyle.Fill;
e.Canvas.DrawRect(e.Bounds, paint);
}
}
// -----------------------------------
// --- Event - Layer - Grid - Draw ---
// -----------------------------------
private void Layer_Grid_Draw(object sender, EventArgs_Draw e)
{
if (m_ShowGrid)
{
// Draw a 25x25 grid of gray lines
using (var paint = new SKPaint())
{
paint.Color = new SKColor(64, 64, 64); // Very dark gray
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
// Draw the Horizontal Grid Lines
for (int i = 0; i < 50; i++)
{
var y = e.Bounds.Height * (i / 25f);
var leftPoint = new SKPoint(e.Bounds.Left, y);
var rightPoint = new SKPoint(e.Bounds.Right, y);
e.Canvas.DrawLine(leftPoint, rightPoint, paint);
}
// Draw the Vertical Grid Lines
for (int i = 0; i < 50; i++)
{
var x = e.Bounds.Width * (i / 25f);
var topPoint = new SKPoint(x, e.Bounds.Top);
var bottomPoint = new SKPoint(x, e.Bounds.Bottom);
e.Canvas.DrawLine(topPoint, bottomPoint, paint);
}
}
}
}
// -----------------------------------
// --- Event - Layer - Date - Draw ---
// -----------------------------------
private void Layer_Data_Draw(object sender, EventArgs_Draw e)
{
// Draw a simple bar graph
// Flip the Y-Axis so that zero is on the bottom
e.Canvas.Scale(1, -1);
e.Canvas.Translate(0, -e.Bounds.Height);
var rand = new Random();
// Create 25 red / yellow gradient bars of random length
for (int i = 0; i < 25; i++)
{
var barWidth = e.Bounds.Width / 25f;
var barHeight = rand.Next((int)(e.Bounds.Height * 0.65d));
var barLeft = (i + 0) * barWidth;
var barRight = (i + 1) * barWidth;
var barTop = barHeight;
var barBottom = 0;
var topLeft = new SKPoint(barLeft, barTop);
var bottomRight = new SKPoint(barRight, barBottom);
var gradColors = new SKColor[2] { SKColors.Yellow, SKColors.Red };
// Draw each bar with a gradient fill
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Style = SKPaintStyle.Fill;
paint.StrokeWidth = 1;
paint.Shader = shader;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
// Draw the border of each bar
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
}
}
// --------------------------------------
// --- Event - Layer - Overlay - Draw ---
// --------------------------------------
private void Layer_Overlay_Draw(object sender, EventArgs_Draw e)
{
// Draw the mouse coordinate text next to the cursor
using (var paint = new SKPaint())
{
// Configure the Paint to draw a black rectangle behind the text
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Fill;
// Measure the bounds of the text
var text = m_MousePos.ToString();
SKRect textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// Fix the inverted height value from the MeaureText
textBounds = textBounds.Standardized;
textBounds.Location = new SKPoint(m_MousePos.X, m_MousePos.Y - textBounds.Height);
// Draw the black filled rectangle where the text will go
e.Canvas.DrawRect(textBounds, paint);
// Change the Paint to yellow
paint.Color = SKColors.Yellow;
// Draw the mouse coordinates text
e.Canvas.DrawText(m_MousePos.ToString(), m_MousePos, paint);
}
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- Class - Layer -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class Layer
{
// The Draw event that the background rendering thread will use to draw on the SKPicture Canvas.
public event EventHandler<EventArgs_Draw> Draw;
// The finished recording - Used to play back the Draw commands to the SKGLControl from the GUI thread
private SKPicture m_Picture = null;
// A flag that indicates if the Layer is valid, or needs to be redrawn.
private bool m_IsValid = false;
// ---------------------------
// --- Layer - Constructor ---
// ---------------------------
public Layer(string title)
{
this.Title = title;
}
// -------------
// --- Title ---
// -------------
public string Title { get; set; }
// --------------
// --- Render ---
// --------------
// Raises the Draw event and records any drawing commands to an SKPicture for later playback.
// This can be called from any thread.
public void Render(SKRect clippingBounds)
{
// Only redraw the Layer if it has been invalidated
if (!m_IsValid)
{
// Create an SKPictureRecorder to record the Canvas Draw commands to an SKPicture
using (var recorder = new SKPictureRecorder())
{
// Start recording
recorder.BeginRecording(clippingBounds);
// Raise the Draw event. The subscriber can then draw on the Canvas provided in the event
// and the commands will be recorded for later playback.
Draw?.Invoke(this, new EventArgs_Draw(recorder.RecordingCanvas, clippingBounds));
// Dispose of any previous Pictures
m_Picture?.Dispose();
// Create a new SKPicture with recorded Draw commands
m_Picture = recorder.EndRecording();
this.RenderCount++;
m_IsValid = true;
}
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been rendered
public int RenderCount { get; private set; }
// -------------
// --- Paint ---
// -------------
// Paints the previously recorded SKPicture to the provided skglControlCanvas. This basically plays
// back the draw commands from the last Render. This should be called from the SKGLControl.PaintSurface
// event using the GUI thread.
public void Paint(SKCanvas skglControlCanvas)
{
if (m_Picture != null)
{
// Play back the previously recorded Draw commands to the skglControlCanvas using the GUI thread
skglControlCanvas.DrawPicture(m_Picture);
this.PaintCount++;
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been painted
public int PaintCount { get; private set; }
// ------------------
// --- Invalidate ---
// ------------------
// Forces the Layer to be redrawn with the next rendering cycle
public void Invalidate()
{
m_IsValid = false;
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- EventArgs - Draw -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class EventArgs_Draw : EventArgs
{
public SKRect Bounds { get; set; }
public SKCanvas Canvas { get; set; }
public EventArgs_Draw(SKCanvas canvas, SKRect bounds)
{
this.Canvas = canvas;
this.Bounds = bounds;
}
}
}
我正在尝试用 SkiaSharp 替换 GDI+,以获得一个数据可视化框架,该框架使用实时不断变化的工程数据呈现多层可平移缩放图形。
在 GDI+ 中,应用程序是这样做的:
- 创建了一组具有透明背景的绘图层,通常是网格层、一个或多个数据层以及用于光标信息和突出显示的覆盖层,每个层都由单独的位图支持。
- 在渲染循环后台线程中,只有需要为每个渲染循环更新的层(位图)使用 GDI+ 重绘。这可能需要数以千计的计算和转换线、矩形和文本来创建热图、波形、直方图、数据标签等。
- 然后,堆栈中的每个绘图层将由后台线程通过 BitBlt 转换为复合位图
- 然后,最终的复合位图将以高达 30fps 的速度绘制到 GUI 线程中的 WinForm PictureBox。
在一个或多个后台线程中完成直到最终图像呈现的所有操作。GUI 线程仅涉及将完成的图像绘制到 PictureBox。这很重要,因为还有许多其他 GUI 控件需要保持响应。这很好用,除了它都是基于 CPU 的。小 windows 没问题,但在 4K 屏幕上最大化会减慢渲染速度,足以使程序几乎无法使用。
我想用 GPU 加速的 SkiaSharp 重新创建这个概念。
我尝试创建了几十个不同的测试程序,但我不断遇到跨线程访问冲突,或者屏幕上什么都不显示,或者硬崩溃。与其贴代码,不如问一些基本问题:
问题:
- 您将如何创建这个框架? SkiaSharp 甚至可以做到这一点吗?
- 我的每个图层 类 是否应该维护一个 SKSurface、SKCanvas、SKImage 或 SKBitmap? - 同样,如果一个图层在当前循环中不需要重绘,那么该图层需要保持之前绘制的内容,以便在下一个合成图像中使用。
- GUI 线程需要 GLControl 和 GRContext 来显示最终的合成图像,但是是否应该有另一个单独的 GRContext 供后台渲染线程使用? - 如何使用 GPU 加速创建?
- 是否有人可以指出类似概念的任何工作示例? (GPU 加速从后台线程到 GLControl 的渲染)
- 我是否应该只使用隐藏在后台的 SkiaSharp,并使用带有 PictureBox 的 GDI+ BitBlt 在屏幕上显示合成图像? - 这会解决一些线程问题吗?
任何帮助定义方法和注意事项将不胜感激!!
我想出了如何使用 SKPicture 对象来使用背景渲染线程记录来自每一层的绘制命令,然后使用 GUI 线程将它们绘制回 SKGLControl。这满足了我的所有要求:它允许多个绘图层,使用后台线程渲染,仅渲染需要更新的层,使用 GPU 加速进行绘制,并且对于最大化 4K window.
经验教训
我一路上学到的一些教训让我很困惑...
网上有使用GPU加速的OpenTK.GLControl的例子,也有使用内置GPU加速的SkiaSharp.Views.Desktop.SKGLControl的例子。 SKGLControl 绝对是这项任务的正确控制。由于 FramebufferBinding 和 StencilBits 的问题,GLControl 正在为 DrawCircle 创建正方形并拒绝渲染任何曲线?!? - 我放弃了。对于 SKPicture 对象,它也比 SKGLControl 慢。
SKGLControl 不需要也不喜欢使用 GLControl 所需的 SwapBuffers 或 Canvas.Flush。这导致了 SKGLControl 的绘图出现频闪和故障,这就是为什么我在与 GLControl 的杂草斗争中脱颖而出。当我用 SKGLControl 重建项目并摆脱 SwapBuffers 和 Canvas.Flush 时,一切开始正常运行。
对表面和画布的引用不应超过一个 PaintSurface 循环。 SKPicture 是一个神奇的对象,它可以让你存储每一层的绘图命令并一次又一次地播放它们。这与 SKBitmap 或 SKImage 不同,它们生成像素光栅而不只是记录绘制命令。我无法让 SKBitmap 或 SKImage 在多线程环境中运行并且仍然是 GPU 加速的。 SKPicture 非常适合这个。
SKGLControl 的 Paint 事件和 PaintSurface 事件是有区别的。 PaintSurface 事件是应该使用的,默认情况下是 GPU 加速的。
工作示例代码
下面是一个多层、多线程、GPU 加速的 SkiaSharp 绘图的全功能演示
此示例创建 4 个绘图层:
- 背景图层
- 网格层
- 数据层
- 叠加层
图层使用后台线程绘制(渲染),然后使用 GUI 线程绘制到 SKGLControl。每个图层只在需要时渲染,但所有图层都是用每个 PaintSurface 事件绘制的。
尝试代码:
- 在 Visual Studio 中创建一个新的 C# WinForms 项目。
- 添加 NuGet 包:“SkiaSharp.Views.WindowsForms”。这将自动添加“SkiaSharp”和“SkiaSharp.Views.Desktop.Common”。
- 向 Form1 添加 SkiaSharp.Views.Desktop.SKGLControl。将其命名为“skglControl1”
- 将 skglControl1 的 Dock 设置为“填充”,使其填充 Form1。
- 将下面的代码复制到Form1:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
namespace SkiaSharp_Multi_Layer_GPU
{
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- WinForm - Form 1 -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public partial class Form1 : Form
{
private Thread m_RenderThread = null;
private AutoResetEvent m_ThreadGate = null;
private List<Layer> m_Layers = null;
private Layer m_Layer_Background = null;
private Layer m_Layer_Grid = null;
private Layer m_Layer_Data = null;
private Layer m_Layer_Overlay = null;
private bool m_KeepSwimming = true;
private SKPoint m_MousePos = new SKPoint();
private bool m_ShowGrid = true;
private Point m_PrevMouseLoc = new Point();
// ---------------------------
// --- Form1 - Constructor ---
// ---------------------------
public Form1()
{
InitializeComponent();
}
// ------------------------------
// --- Event - Form1 - OnLoad ---
// ------------------------------
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Set the title of the Form
this.Text = "SkiaSharp Demo - Multi-Layer, Multi-Threaded, GPU Accelerated";
// Create layers to draw on, each with a dedicated SKPicture
m_Layer_Background = new Layer("Background Layer");
m_Layer_Grid = new Layer("Grid Layer");
m_Layer_Data = new Layer("Data Layer");
m_Layer_Overlay = new Layer("Overlay Layer");
// Create a collection for the drawing layers
m_Layers = new List<Layer>();
m_Layers.Add(m_Layer_Background);
m_Layers.Add(m_Layer_Grid);
m_Layers.Add(m_Layer_Data);
m_Layers.Add(m_Layer_Overlay);
// Subscribe to the Draw Events for each layer
m_Layer_Background.Draw += Layer_Background_Draw;
m_Layer_Grid.Draw += Layer_Grid_Draw;
m_Layer_Data.Draw += Layer_Data_Draw;
m_Layer_Overlay.Draw += Layer_Overlay_Draw;
// Subscribe to the SKGLControl events
skglControl1.PaintSurface += SkglControl1_PaintSurface;
skglControl1.Resize += SkglControl1_Resize;
skglControl1.MouseMove += SkglControl1_MouseMove;
skglControl1.MouseDoubleClick += SkglControl1_MouseDoubleClick;
// Create a background rendering thread
m_RenderThread = new Thread(RenderLoopMethod);
m_ThreadGate = new AutoResetEvent(false);
// Start the rendering thread
m_RenderThread.Start();
}
// ---------------------------------
// --- Event - Form1 - OnClosing ---
// ---------------------------------
protected override void OnClosing(CancelEventArgs e)
{
// Let the rendering thread terminate
m_KeepSwimming = false;
m_ThreadGate.Set();
base.OnClosing(e);
}
// --------------------------------------------
// --- Event - SkglControl1 - Paint Surface ---
// --------------------------------------------
private void SkglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
{
// Clear the Canvas
e.Surface.Canvas.Clear(SKColors.Black);
// Paint each pre-rendered layer onto the Canvas using this GUI thread
foreach (var layer in m_Layers)
{
layer.Paint(e.Surface.Canvas);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.LimeGreen;
for (int i = 0; i < m_Layers.Count; i++)
{
var layer = m_Layers[i];
var text = $"{layer.Title} - Renders = {layer.RenderCount}, Paints = {layer.PaintCount}";
var textLoc = new SKPoint(10, 10 + (i * 15));
e.Surface.Canvas.DrawText(text, textLoc, paint);
}
paint.Color = SKColors.Cyan;
e.Surface.Canvas.DrawText("Click-Drag to update bars.", new SKPoint(10, 80), paint);
e.Surface.Canvas.DrawText("Double-Click to show / hide grid.", new SKPoint(10, 95), paint);
e.Surface.Canvas.DrawText("Resize to update all.", new SKPoint(10, 110), paint);
}
}
// -------------------------------------
// --- Event - SkglControl1 - Resize ---
// -------------------------------------
private void SkglControl1_Resize(object sender, EventArgs e)
{
// Invalidate all of the Layers
foreach (var layer in m_Layers)
{
layer.Invalidate();
}
// Start a new rendering cycle to redraw all of the layers.
UpdateDrawing();
}
// -----------------------------------------
// --- Event - SkglControl1 - Mouse Move ---
// -----------------------------------------
private void SkglControl1_MouseMove(object sender, MouseEventArgs e)
{
// Save the mouse position
m_MousePos = e.Location.ToSKPoint();
// If Left-Click Drag, draw new bars
if (e.Button == MouseButtons.Left)
{
// Invalidate the Data Layer to draw a new random set of bars
m_Layer_Data.Invalidate();
}
// If Mouse Move, draw new mouse coordinates
if (e.Location != m_PrevMouseLoc)
{
// Remember the previous mouse location
m_PrevMouseLoc = e.Location;
// Invalidate the Overlay Layer to show the new mouse coordinates
m_Layer_Overlay.Invalidate();
}
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// -------------------------------------------------
// --- Event - SkglControl1 - Mouse Double Click ---
// -------------------------------------------------
private void SkglControl1_MouseDoubleClick(object sender, MouseEventArgs e)
{
// Toggle the grid visibility
m_ShowGrid = !m_ShowGrid;
// Invalidate only the Grid Layer.
m_Layer_Grid.Invalidate();
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// ----------------------
// --- Update Drawing ---
// ----------------------
public void UpdateDrawing()
{
// Unblock the rendering thread to begin a render cycle. Only the invalidated
// Layers will be re-rendered, but all will be repainted onto the SKGLControl.
m_ThreadGate.Set();
}
// --------------------------
// --- Render Loop Method ---
// --------------------------
private void RenderLoopMethod()
{
while (m_KeepSwimming)
{
// Draw any invalidated layers using this Render thread
DrawLayers();
// Invalidate the SKGLControl to run the PaintSurface event on the GUI thread
// The PaintSurface event will Paint the layer stack to the SKGLControl
skglControl1.Invalidate();
// DoEvents to ensure that the GUI has time to process
Application.DoEvents();
// Block and wait for the next rendering cycle
m_ThreadGate.WaitOne();
}
}
// -------------------
// --- Draw Layers ---
// -------------------
private void DrawLayers()
{
// Iterate through the collection of layers and raise the Draw event for each layer that is
// invalidated. Each event handler will receive a Canvas to draw on along with the Bounds for
// the Canvas, and can then draw the contents of that layer. The Draw commands are recorded and
// stored in an SKPicture for later playback to the SKGLControl. This method can be called from
// any thread.
var clippingBounds = skglControl1.ClientRectangle.ToSKRect();
foreach (var layer in m_Layers)
{
layer.Render(clippingBounds);
}
}
// -----------------------------------------
// --- Event - Layer - Background - Draw ---
// -----------------------------------------
private void Layer_Background_Draw(object sender, EventArgs_Draw e)
{
// Create a diagonal gradient fill from Blue to Black to use as the background
var topLeft = new SKPoint(e.Bounds.Left, e.Bounds.Top);
var bottomRight = new SKPoint(e.Bounds.Right, e.Bounds.Bottom);
var gradColors = new SKColor[2] { SKColors.DarkBlue, SKColors.Black };
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Shader = shader;
paint.Style = SKPaintStyle.Fill;
e.Canvas.DrawRect(e.Bounds, paint);
}
}
// -----------------------------------
// --- Event - Layer - Grid - Draw ---
// -----------------------------------
private void Layer_Grid_Draw(object sender, EventArgs_Draw e)
{
if (m_ShowGrid)
{
// Draw a 25x25 grid of gray lines
using (var paint = new SKPaint())
{
paint.Color = new SKColor(64, 64, 64); // Very dark gray
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
// Draw the Horizontal Grid Lines
for (int i = 0; i < 50; i++)
{
var y = e.Bounds.Height * (i / 25f);
var leftPoint = new SKPoint(e.Bounds.Left, y);
var rightPoint = new SKPoint(e.Bounds.Right, y);
e.Canvas.DrawLine(leftPoint, rightPoint, paint);
}
// Draw the Vertical Grid Lines
for (int i = 0; i < 50; i++)
{
var x = e.Bounds.Width * (i / 25f);
var topPoint = new SKPoint(x, e.Bounds.Top);
var bottomPoint = new SKPoint(x, e.Bounds.Bottom);
e.Canvas.DrawLine(topPoint, bottomPoint, paint);
}
}
}
}
// -----------------------------------
// --- Event - Layer - Date - Draw ---
// -----------------------------------
private void Layer_Data_Draw(object sender, EventArgs_Draw e)
{
// Draw a simple bar graph
// Flip the Y-Axis so that zero is on the bottom
e.Canvas.Scale(1, -1);
e.Canvas.Translate(0, -e.Bounds.Height);
var rand = new Random();
// Create 25 red / yellow gradient bars of random length
for (int i = 0; i < 25; i++)
{
var barWidth = e.Bounds.Width / 25f;
var barHeight = rand.Next((int)(e.Bounds.Height * 0.65d));
var barLeft = (i + 0) * barWidth;
var barRight = (i + 1) * barWidth;
var barTop = barHeight;
var barBottom = 0;
var topLeft = new SKPoint(barLeft, barTop);
var bottomRight = new SKPoint(barRight, barBottom);
var gradColors = new SKColor[2] { SKColors.Yellow, SKColors.Red };
// Draw each bar with a gradient fill
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Style = SKPaintStyle.Fill;
paint.StrokeWidth = 1;
paint.Shader = shader;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
// Draw the border of each bar
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
}
}
// --------------------------------------
// --- Event - Layer - Overlay - Draw ---
// --------------------------------------
private void Layer_Overlay_Draw(object sender, EventArgs_Draw e)
{
// Draw the mouse coordinate text next to the cursor
using (var paint = new SKPaint())
{
// Configure the Paint to draw a black rectangle behind the text
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Fill;
// Measure the bounds of the text
var text = m_MousePos.ToString();
SKRect textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// Fix the inverted height value from the MeaureText
textBounds = textBounds.Standardized;
textBounds.Location = new SKPoint(m_MousePos.X, m_MousePos.Y - textBounds.Height);
// Draw the black filled rectangle where the text will go
e.Canvas.DrawRect(textBounds, paint);
// Change the Paint to yellow
paint.Color = SKColors.Yellow;
// Draw the mouse coordinates text
e.Canvas.DrawText(m_MousePos.ToString(), m_MousePos, paint);
}
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- Class - Layer -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class Layer
{
// The Draw event that the background rendering thread will use to draw on the SKPicture Canvas.
public event EventHandler<EventArgs_Draw> Draw;
// The finished recording - Used to play back the Draw commands to the SKGLControl from the GUI thread
private SKPicture m_Picture = null;
// A flag that indicates if the Layer is valid, or needs to be redrawn.
private bool m_IsValid = false;
// ---------------------------
// --- Layer - Constructor ---
// ---------------------------
public Layer(string title)
{
this.Title = title;
}
// -------------
// --- Title ---
// -------------
public string Title { get; set; }
// --------------
// --- Render ---
// --------------
// Raises the Draw event and records any drawing commands to an SKPicture for later playback.
// This can be called from any thread.
public void Render(SKRect clippingBounds)
{
// Only redraw the Layer if it has been invalidated
if (!m_IsValid)
{
// Create an SKPictureRecorder to record the Canvas Draw commands to an SKPicture
using (var recorder = new SKPictureRecorder())
{
// Start recording
recorder.BeginRecording(clippingBounds);
// Raise the Draw event. The subscriber can then draw on the Canvas provided in the event
// and the commands will be recorded for later playback.
Draw?.Invoke(this, new EventArgs_Draw(recorder.RecordingCanvas, clippingBounds));
// Dispose of any previous Pictures
m_Picture?.Dispose();
// Create a new SKPicture with recorded Draw commands
m_Picture = recorder.EndRecording();
this.RenderCount++;
m_IsValid = true;
}
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been rendered
public int RenderCount { get; private set; }
// -------------
// --- Paint ---
// -------------
// Paints the previously recorded SKPicture to the provided skglControlCanvas. This basically plays
// back the draw commands from the last Render. This should be called from the SKGLControl.PaintSurface
// event using the GUI thread.
public void Paint(SKCanvas skglControlCanvas)
{
if (m_Picture != null)
{
// Play back the previously recorded Draw commands to the skglControlCanvas using the GUI thread
skglControlCanvas.DrawPicture(m_Picture);
this.PaintCount++;
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been painted
public int PaintCount { get; private set; }
// ------------------
// --- Invalidate ---
// ------------------
// Forces the Layer to be redrawn with the next rendering cycle
public void Invalidate()
{
m_IsValid = false;
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- EventArgs - Draw -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class EventArgs_Draw : EventArgs
{
public SKRect Bounds { get; set; }
public SKCanvas Canvas { get; set; }
public EventArgs_Draw(SKCanvas canvas, SKRect bounds)
{
this.Canvas = canvas;
this.Bounds = bounds;
}
}
}