这次继承C# Avalonia官方自带的Canvas,扩展一个InkCanvas,兼容Canvas的所有功能。为了简化自定义命名控件,建议把自定义控件加入到默认空间。

AssemblyInfo.cs代码如下

using System.Runtime.CompilerServices;
using System.Resources;
using Avalonia.Metadata; [assembly: NeutralResourcesLanguage("zh-CN")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Shares.Avalonia")]

Canvas类有几点需要注意。

1. 自定义内容区域,是通过[Content]属性来描述Controls类。

        [Content]
public Controls Children { get; } = new Controls();

2. Render是sealed,所以不支持重写Render。

        public sealed override void Render(DrawingContext context)

现在,我们在Shares.Avalonia共享项目中,创建一个ControlExtensions.cs,实现InkCanvas类。代码如下

using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using System.Linq; namespace Shares.Avalonia
{
public enum InkEditingMode
{
Ink,
Erase,
Select
} public class InkStroke
{
public List<Point> Points { get; set; } = new();
public Color Color { get; set; } = Colors.Black;
public double Thickness { get; set; } = 1.0;
} public class InkCanvasLayer : Control
{
public List<InkStroke> Strokes { get; set; } = new();
public InkStroke? CurrentStroke { get; set; }
public List<InkStroke> SelectedStrokes { get; set; } = new();
public Rect? SelectionRect { get; set; }
public Rect? SelectionBox { get; set; } public override void Render(DrawingContext context)
{
base.Render(context); foreach (var stroke in Strokes)
{
var isSelected = SelectedStrokes.Contains(stroke);
DrawStroke(context, stroke, isSelected);
} if (CurrentStroke != null)
DrawStroke(context, CurrentStroke); if (SelectionRect.HasValue)
{
context.DrawRectangle(null,
new Pen(Brushes.DarkOliveGreen, 1, dashStyle: DashStyle.Dash),
SelectionRect.Value);
} if (SelectionBox.HasValue)
{
var pen = new Pen(Brushes.DarkGray, 1, dashStyle: DashStyle.Dash);
context.DrawRectangle(null, pen, SelectionBox.Value);
}
} private void DrawStroke(DrawingContext context, InkStroke stroke, bool isSelected = false)
{
if (stroke.Points.Count < 2) return; var color = isSelected ? Colors.Black : stroke.Color;
var thickness = isSelected ? stroke.Thickness * 2 : stroke.Thickness;
var pen = new Pen(new SolidColorBrush(color), thickness);
for (int i = 1; i < stroke.Points.Count; i++)
{
context.DrawLine(pen, stroke.Points[i - 1], stroke.Points[i]);
}
}
} public class InkCanvas : Canvas
{
private readonly InkCanvasLayer layer;
private List<InkStroke> strokes = new();
private InkStroke? currentStroke;
private Stack<List<InkStroke>> undoStack = new();
private Stack<List<InkStroke>> redoStack = new(); private bool isSelecting = false;
private Rect selectionRect;
private List<InkStroke> selectedStrokes = new();
private Point selectionStart; private bool isDraggingSelection = false;
private Point lastDragPoint; public static readonly StyledProperty<Color> StrokeColorProperty =
AvaloniaProperty.Register<InkCanvas, Color>(nameof(StrokeColor), Colors.Black);
public Color StrokeColor
{
get => GetValue(StrokeColorProperty);
set => SetValue(StrokeColorProperty, value);
} public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<InkCanvas, double>(nameof(StrokeThickness), 2.0);
public double StrokeThickness
{
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
} public static readonly StyledProperty<InkEditingMode> EditingModeProperty =
AvaloniaProperty.Register<InkCanvas, InkEditingMode>(nameof(EditingMode), InkEditingMode.Ink);
public InkEditingMode EditingMode
{
get => GetValue(EditingModeProperty);
set => SetValue(EditingModeProperty, value);
} public InkCanvas()
{
layer = new InkCanvasLayer();
Children.Add(layer); PointerPressed += OnPointerPressed;
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased; this.GetObservable(EditingModeProperty).Subscribe(mode =>
{
selectedStrokes.Clear();
layer.SelectedStrokes = selectedStrokes;
layer.SelectionBox = null;
layer.InvalidateVisual();
}); Background = Brushes.White;
} protected override Size ArrangeOverride(Size finalSize)
{
layer.Arrange(new Rect(finalSize));
return base.ArrangeOverride(finalSize);
} private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var point = e.GetPosition(this); if (EditingMode == InkEditingMode.Erase)
{
EraseAtPoint(point);
return;
} if (EditingMode == InkEditingMode.Select)
{
selectionStart = point;
selectionRect = new Rect(point, point); if (selectedStrokes.Any(s => s.Points.Any(p => Distance(p, point) < 5)))
{
isDraggingSelection = true;
lastDragPoint = point;
return;
} isSelecting = true;
return;
} currentStroke = new InkStroke
{
Color = StrokeColor,
Thickness = StrokeThickness
};
currentStroke.Points.Add(point);
layer.CurrentStroke = currentStroke;
e.Pointer.Capture(this);
} private void OnPointerMoved(object? sender, PointerEventArgs e)
{
var point = e.GetPosition(this); if (currentStroke != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
currentStroke.Points.Add(point);
layer.InvalidateVisual();
} if (isDraggingSelection && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var delta = point - lastDragPoint;
MoveSelected(delta);
lastDragPoint = point;
UpdateSelectionBox();
} if (isSelecting && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
selectionRect = new Rect(selectionStart, point).Normalize();
layer.SelectionRect = selectionRect;
layer.InvalidateVisual();
}
} private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (currentStroke != null)
{
SaveUndoState();
strokes.Add(currentStroke);
currentStroke = null;
layer.CurrentStroke = null;
} if (isDraggingSelection)
{
isDraggingSelection = false;
} if (isSelecting)
{
isSelecting = false;
layer.SelectionRect = null;
SelectStrokesInRect(selectionRect);
} layer.InvalidateVisual();
e.Pointer.Capture(null);
} private void SelectStrokesInRect(Rect rect)
{
selectedStrokes.Clear();
foreach (var stroke in strokes)
{
if (stroke.Points.Any(p => rect.Contains(p)))
{
selectedStrokes.Add(stroke);
}
}
layer.SelectedStrokes = selectedStrokes;
UpdateSelectionBox();
} private void UpdateSelectionBox()
{
if (selectedStrokes.Count == 0)
{
layer.SelectionBox = null;
return;
} double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue; foreach (var stroke in selectedStrokes)
{
foreach (var p in stroke.Points)
{
minX = Math.Min(minX, p.X);
minY = Math.Min(minY, p.Y);
maxX = Math.Max(maxX, p.X);
maxY = Math.Max(maxY, p.Y);
}
} layer.SelectionBox = new Rect(minX, minY, maxX - minX, maxY - minY);
} private void EraseAtPoint(Point point)
{
const double hitRadius = 5;
SaveUndoState();
strokes.RemoveAll(s => s.Points.Exists(p => Distance(p, point) < hitRadius));
layer.Strokes = strokes;
layer.InvalidateVisual();
} private double Distance(Point a, Point b)
{
var dx = a.X - b.X;
var dy = a.Y - b.Y;
return Math.Sqrt(dx * dx + dy * dy);
} public void MoveSelected(Vector delta)
{
foreach (var stroke in selectedStrokes)
{
for (int i = 0; i < stroke.Points.Count; i++)
stroke.Points[i] += delta;
}
UpdateSelectionBox();
layer.InvalidateVisual();
} private void SaveUndoState()
{
undoStack.Push(strokes.Select(s => new InkStroke
{
Points = new List<Point>(s.Points),
Color = s.Color,
Thickness = s.Thickness
}).ToList());
redoStack.Clear();
layer.Strokes = strokes;
} public void Undo()
{
if (undoStack.Count == 0) return;
redoStack.Push(strokes);
strokes = undoStack.Pop();
layer.Strokes = strokes;
layer.InvalidateVisual();
} public void Redo()
{
if (redoStack.Count == 0) return;
undoStack.Push(strokes);
strokes = redoStack.Pop();
layer.Strokes = strokes;
layer.InvalidateVisual();
} public IReadOnlyList<InkStroke> Strokes => strokes.AsReadOnly();
}
}

SimpleInkCanvas.axaml代码,其中office.jpg要把属性设置为AvaloniaResource。目前AvaloniaResource除了对axaml有bug外,其他资源是没问题。

<Window xmlns="https://github.com/avaloniaui"
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"
Height="300" Width="300"
x:Class="AvaloniaUI.SimpleInkCanvas"
Title="SimpleInkCanvas">
<Grid RowDefinitions="auto,*">
<StackPanel Margin="5" Orientation="Horizontal">
<TextBlock Margin="5">EditingMode: </TextBlock>
<ComboBox Name="lstEditingMode" VerticalAlignment="Center">
</ComboBox>
</StackPanel> <InkCanvas Name="inkCanvas" Grid.Row="1" Background="LightYellow" EditingMode="{Binding ElementName=lstEditingMode,Path=SelectedItem}">
<Button Canvas.Top="10" Canvas.Left="10">Hello</Button>
<Image Source="avares://AvaloniaUI/Resources/Images/office.jpg" Canvas.Top="10" Canvas.Left="50"
Width="100" Height="100"/>
</InkCanvas>
</Grid>
</Window>

SimpleInkCanvas.axaml.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Shares.Avalonia;
using System; namespace AvaloniaUI; public partial class SimpleInkCanvas : Window
{
public SimpleInkCanvas()
{
InitializeComponent(); foreach (InkEditingMode mode in Enum.GetValues(typeof(InkEditingMode)))
{
lstEditingMode.Items.Add(mode);
lstEditingMode.SelectedItem = inkCanvas.EditingMode;
}
}
}

运行效果

C# Avalonia 03 - LayoutPanels - SimpleInkCanvas的更多相关文章

  1. Android游戏开发实践(1)之NDK与JNI开发03

    Android游戏开发实践(1)之NDK与JNI开发03 前面已经分享了两篇有关Android平台NDK与JNI开发相关的内容.以下列举前面两篇的链接地址,感兴趣的可以再回顾下.那么,这篇继续这个小专 ...

  2. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  3. iOS系列 基础篇 03 探究应用生命周期

    iOS系列 基础篇 03 探究应用生命周期 目录: 1. 非运行状态 - 应用启动场景 2. 点击Home键 - 应用退出场景 3. 挂起重新运行场景 4. 内存清除 - 应用终止场景 5. 结尾 本 ...

  4. javaSE基础03

    javaSE基础03 生活中常见的进制:十进制(0-9).星期(七进制(0-6)).时间(十二进制(0-11)).二十四进制(0-23) 进制之间的转换: 十进制转为二进制: 将十进制除以2,直到商为 ...

  5. UML大战需求分析——阅读笔记03

    读<UML大战需求分析>有感03 状态机图和活动图在样子比较相似,但状态机图是用来为对象的状态及造成状态改变的事件建模.我们大二学习UML统一建模语言状态机图模块时了解到,UML的状态机图 ...

  6. 2016-1-28 图解HTTP(03)

    6.2.5 非HTTP/1.1首部字段        不限于RFC2616中定义的47种首部字段,还有Cookie.Set-Cookie和Content-Disposition等在其他RFC中首部字段 ...

  7. ReactNative新手学习之路03真机调试

    React Native新手入门03真机调试(iOS) 从设备访问开发服务器 在启用开发服务器的情况下,你可以快速的迭代修改应用,然后在设备上查看结果.这样做的前提是你的电脑和设备必须在同一个wifi ...

  8. 【三石jQuery视频教程】03.创建垂直时间表(Timeline)

    视频地址:http://v.qq.com/page/g/i/o/g0150rvi6io.html 大家好,欢迎来到[三石jQuery视频教程],我是您的老朋友 - 三生石上. 今天,我们要通过基本的H ...

  9. javascript基础03

    javascript基础03 1. 算术运算符 后增量/后减量运算符 ++ ,-- 比较运算符 ( >, <, >=, <=, ==, !=,===,!== ) 逻辑运算符( ...

  10. service 03 iis之服务器无访问权限

    这两天在Service 03 的iis 6.0 里面配置一个aspx 的网站 ,总是遇到一个问题  401.2   无权限访问,于是去百度了一下好多的方法,基本上是关于设置匿名用户,打开IUSER用户 ...

随机推荐

  1. 学习Kotlin语法(三)

    简介 在上一节,我们对Kotlin中面向对象编程(OOP)的相关知识有了大致的了解,本章节我们将去进一步了解函数.lambada表达式.内联函数.操作符重载.作用域函数. 目录 函数 函数的使用 参数 ...

  2. 2024 (ICPC) Jiangxi Provincial Contest -- Official Contest

    L. Campus 1.首先考虑时间复杂度,因为最多只会有2*k的时间点,所以我们采取的策略是,对这每个时刻,判断有多少扇门是开的,并且考虑这些门到其他点的最短路之和. 2.输入完数据以后,使用dij ...

  3. CH9121default与classical设置方法

    SYN发送间隔调整方法: 网口连接设备后双击设备列表中要配置的设备在扩展参数中单击获取扩展参数,在超时处理模式选项选择Classical然后执行设置扩展参数,最后点击复位模块后生效(仅TCP CLIE ...

  4. STM32_RTOS_V2编程模板1-消息队列

    #pragma region QUEUE1 // 1DEFINE osMessageQueueId_t queueDemo1 = NULL; // 2INIT queueDemo1 = osMessa ...

  5. vue2鼠标事件

    1.单击 @click 2.按下 @mousedown 3.抬起 @mouseup 4.双击 @dblclick 5.移动 @mousemove 6.移除 @mouseout 7.离开 @mousel ...

  6. python,url请求失败重新请求的方法(try、except 应用)

    爬虫请求链接,有时候会出现请求失败或者等待时间很长的情况,用下面的方法可以一定程度的解决这个问题 url='https://cl.xxxx.xyz/'+url try: response = requ ...

  7. 基于Zabbix Low-level discovery 方式 网络质量监控配置手册

  8. 3d xna fbx winfrom 读取

    本文通过参考网上资源做的一个例子. 本程序的功能就是通过xna 将3d 图像显示到winfrom 对他进行旋转操作. 首先我们先准备好两个文件夹 model  文件夹放fbx文件,textures 放 ...

  9. Java Objects.equals(a,b)的说明

    一:值是null的情况: a.equals(b), a 是null, 抛出NullPointException异常. a.equals(b), a不是null, b是null, 返回false Obj ...

  10. Scroll滚动,你玩明白了嘛?

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...