03 - LayoutPanels例子 - SimpleInkCanvas
C# maui暂时没有官方支持InkCanvas,但是不影响,自己实现一个就行了。目前支持,画图,选择,移动和删除。同时支持自定义橡皮擦形状,也支持绑定自定义的形状列表。
实现一个Converter类,以后所有的绑定类型转换都在这个类中实现。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace Shares.Utility
{
public class Converter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
// Implement conversion logic here
if (value is List<string> list)
{
return string.Join(", ", list); // 自定义分隔符
}
else if (value is int intValue && targetType.IsEnum)
{
return Enum.ToObject(targetType, intValue); // 将整数转换为枚举类型
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
// Implement conversion back logic here
return value;
}
}
}
然后在MyStyles.xaml中添加Converter类的引用,这样以后所有项目都可以使用了,local是
xmlns:local="clr-namespace:Shares.Utility;assembly=Shares"
<!--converter定义-->
<local:Converter x:Key="Converter"/>
InkCanvas重写GraphicsView
public class InkCanvas : GraphicsView, IDrawable
{
public class DrawingPath
{
private RectF? cachedBounds;
private bool isDirty = true; public Guid Id { get; } = Guid.NewGuid();
public PathF Path { get; set; } = new PathF();
public Color? StrokeColor { get; set; }
public float StrokeThickness { get; set; }
public bool IsSelected { get; set; }
public PointF Pos { get; set; } public RectF Bounds
{
get
{
if (!isDirty && cachedBounds.HasValue)
return cachedBounds.Value; if (Path.Count == 0)
{
cachedBounds = RectF.Zero;
return RectF.Zero;
} var points = Path.Points;
float minX = float.MaxValue, minY = float.MaxValue;
float maxX = float.MinValue, maxY = float.MinValue; foreach (var point in points)
{
float x = point.X + Pos.X;
float y = point.Y + Pos.Y;
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
maxX = Math.Max(maxX, x);
maxY = Math.Max(maxY, y);
} cachedBounds = new RectF(minX, minY, maxX - minX, maxY - minY);
isDirty = false;
return cachedBounds.Value;
}
} public void InvalidateBounds() => isDirty = true; public void LineTo(float x, float y)
{
Path.LineTo(x, y);
InvalidateBounds();
} public bool IntersectAt(PointF eraserPos, float eraserRadius)
{
if (Path.Count == 0)
return false; // 优化点接触检查
foreach (var point in Path.Points)
{
float dx = point.X + Pos.X - eraserPos.X;
float dy = point.Y + Pos.Y - eraserPos.Y;
if (dx * dx + dy * dy <= eraserRadius * eraserRadius)
{
return true;
}
} // 优化线段接触检查
if (Path.Count >= 2)
{
var points = Path.Points;
for (int i = 1; i < points.Count(); i++)
{
var start = new PointF(points.ElementAt(i - 1).X + Pos.X, points.ElementAt(i - 1).Y + Pos.Y);
var end = new PointF(points.ElementAt(i).X + Pos.X, points.ElementAt(i).Y + Pos.Y); if (PointToLineDistance(start, end, eraserPos) <= eraserRadius)
{
return true;
}
}
} return false;
} public List<DrawingPath> SplitAt(PointF eraserPos, float eraserRadius)
{
var newPaths = new List<DrawingPath>();
if (Path.Count < 2) return newPaths; var points = Path.Points;
int bestIndex = -1;
float minDistance = float.MaxValue; // 1. 检查点接触
for (int i = 0; i < points.Count(); i++)
{
float dx = points.ElementAt(i).X + Pos.X - eraserPos.X;
float dy = points.ElementAt(i).Y + Pos.Y - eraserPos.Y;
float distance = dx * dx + dy * dy; if (distance < minDistance)
{
minDistance = distance;
bestIndex = i;
}
} // 点接触处理
if (bestIndex >= 0 && minDistance <= eraserRadius * eraserRadius)
{
// 起点处理
if (bestIndex == 0)
{
if (points.Count() > 1)
{
var newPath = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
newPath.Path.MoveTo(points.ElementAt(1));
for (int i = 2; i < points.Count(); i++)
{
newPath.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(newPath);
}
return newPaths;
} // 终点处理
if (bestIndex == points.Count() - 1)
{
if (points.Count() > 1)
{
var newPath = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
newPath.Path.MoveTo(points.ElementAt(0));
for (int i = 1; i < points.Count() - 1; i++)
{
newPath.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(newPath);
}
return newPaths;
} // 中间点处理
if (bestIndex > 0 && bestIndex < points.Count() - 1)
{
// 第一段路径
var path1 = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
path1.Path.MoveTo(points.ElementAt(0));
for (int i = 1; i <= bestIndex; i++)
{
path1.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(path1); // 第二段路径
var path2 = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
path2.Path.MoveTo(points.ElementAt(bestIndex));
for (int i = bestIndex + 1; i < points.Count(); i++)
{
path2.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(path2); return newPaths;
}
} // 2. 线段接触处理
bestIndex = -1;
minDistance = float.MaxValue; for (int i = 1; i < points.Count(); i++)
{
var start = new PointF(points.ElementAt(i - 1).X + Pos.X, points.ElementAt(i - 1).Y + Pos.Y);
var end = new PointF(points.ElementAt(i).X + Pos.X, points.ElementAt(i).Y + Pos.Y); float distance = PointToLineDistance(start, end, eraserPos);
if (distance < minDistance)
{
minDistance = distance;
bestIndex = i;
}
} if (bestIndex > 0 && minDistance <= eraserRadius)
{
// 第一段路径
if (bestIndex > 1)
{
var path1 = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
path1.Path.MoveTo(points.ElementAt(0));
for (int i = 1; i < bestIndex; i++)
{
path1.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(path1);
} // 第二段路径
if (bestIndex < points.Count() - 1)
{
var path2 = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = Pos
};
path2.Path.MoveTo(points.ElementAt(bestIndex));
for (int i = bestIndex + 1; i < points.Count(); i++)
{
path2.Path.LineTo(points.ElementAt(i));
}
newPaths.Add(path2);
}
} return newPaths;
}
} public enum InkCanvasEditingMode { Ink, Select, Erase } public static readonly BindableProperty EditingModeProperty =
BindableProperty.Create(nameof(EditingMode), typeof(InkCanvasEditingMode), typeof(InkCanvas),
InkCanvasEditingMode.Ink, BindingMode.TwoWay, propertyChanged: OnEditingModeChanged); private static void OnEditingModeChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is InkCanvas canvas)
{
canvas.ClearSelection();
canvas.Invalidate();
}
} public InkCanvasEditingMode EditingMode
{
get => (InkCanvasEditingMode)GetValue(EditingModeProperty);
set => SetValue(EditingModeProperty, value);
} public ObservableCollection<DrawingPath> Paths { get; set; } = new ObservableCollection<DrawingPath>();
public DrawingPath Eraser { get; }
public float EraserRadius { get; set; } = 15f; // 增大橡皮擦半径
private DrawingPath? currentPath;
private RectF? selectionRect;
private PointF lastTouchPoint;
private bool isMovingSelection; // 橡皮擦轨迹跟踪
private readonly List<PointF> eraserTrail = new List<PointF>();
private const int MaxEraserTrailPoints = 5; public Color StrokeColor { get; set; } = Colors.Black;
public Color SelectionColor { get; set; } = Colors.Red;
public float SelectionStrokeThickness { get; set; } = 1f;
public float StrokeThickness { get; set; } = 1f; public InkCanvas()
{
Drawable = this;
BackgroundColor = Colors.Transparent;
Eraser = CreateEraserPath(); StartInteraction += OnTouchStarted;
DragInteraction += OnTouchMoved;
EndInteraction += OnTouchEnded;
} private DrawingPath CreateEraserPath()
{
var path = new PathF();
var points = new[]
{
new PointF(107.4f, 13), new PointF(113.7f, 28.8f),
new PointF(127.9f, 31.3f), new PointF(117.6f, 43.5f),
new PointF(120.1f, 60.8f), new PointF(107.4f, 52.6f),
new PointF(94.6f, 60.8f), new PointF(97.1f, 43.5f),
new PointF(86.8f, 31.3f), new PointF(101f, 28.8f)
}; path.MoveTo(points[0]);
for (int i = 1; i < points.Length; i++)
{
path.LineTo(points[i]);
}
path.Close(); return new DrawingPath { Path = path, StrokeColor = Colors.Black, StrokeThickness = 1f };
} private void OnTouchStarted(object? sender, TouchEventArgs e)
{
if (e.Touches.Length == 0) return; var point = e.Touches[0];
lastTouchPoint = new PointF(point.X, point.Y);
eraserTrail.Clear(); // 清除历史轨迹 switch (EditingMode)
{
case InkCanvasEditingMode.Ink:
StartInking(lastTouchPoint);
break; case InkCanvasEditingMode.Select:
StartSelection(lastTouchPoint);
break; case InkCanvasEditingMode.Erase:
StartErase(lastTouchPoint);
eraserTrail.Add(lastTouchPoint); // 添加起始点
break;
}
Invalidate();
} private void StartInking(PointF startPoint)
{
currentPath = new DrawingPath
{
StrokeColor = StrokeColor,
StrokeThickness = StrokeThickness,
Pos = PointF.Zero
};
currentPath.Path.MoveTo(startPoint.X, startPoint.Y);
Paths.Add(currentPath);
} private void StartSelection(PointF startPoint)
{
isMovingSelection = Paths.Any(p => p.IsSelected && p.Bounds.Contains(startPoint)); if (!isMovingSelection)
{
ClearSelection();
var clickedPath = Paths.LastOrDefault(p => p.Bounds.Contains(startPoint)); if (clickedPath != null)
{
clickedPath.IsSelected = true;
isMovingSelection = true;
}
else
{
selectionRect = new RectF(startPoint, SizeF.Zero);
}
}
} private void StartErase(PointF startPoint)
{
Eraser.Pos = new PointF(startPoint.X - Eraser.Path.Bounds.Width / 2,
startPoint.Y - Eraser.Path.Bounds.Height / 4);
Eraser.IsSelected = true;
} private void OnTouchMoved(object? sender, TouchEventArgs e)
{
if (e.Touches.Length == 0) return; var currentPoint = new PointF(e.Touches[0].X, e.Touches[0].Y); switch (EditingMode)
{
case InkCanvasEditingMode.Ink:
ContinueInking(currentPoint);
break; case InkCanvasEditingMode.Select:
UpdateSelection(currentPoint);
break; case InkCanvasEditingMode.Erase:
UpdateEraser(currentPoint);
ErasePaths();
break;
}
Invalidate();
} private void ContinueInking(PointF currentPoint)
{
if (currentPath == null) return; const float minDistance = 1.0f;
float dx = currentPoint.X - lastTouchPoint.X;
float dy = currentPoint.Y - lastTouchPoint.Y; if (dx * dx + dy * dy > minDistance * minDistance)
{
currentPath.LineTo(currentPoint.X, currentPoint.Y);
lastTouchPoint = currentPoint;
}
} private void UpdateSelection(PointF currentPoint)
{
if (isMovingSelection)
{
MoveSelectedPaths(currentPoint);
}
else if (selectionRect.HasValue)
{
UpdateSelectionRect(currentPoint);
}
} private void UpdateEraser(PointF currentPoint)
{
Eraser.Pos = new PointF(currentPoint.X - Eraser.Path.Bounds.Width / 2,
currentPoint.Y - Eraser.Path.Bounds.Height / 4); // 添加到橡皮擦轨迹
eraserTrail.Add(Eraser.Pos);
if (eraserTrail.Count > MaxEraserTrailPoints)
{
eraserTrail.RemoveAt(0);
} lastTouchPoint = currentPoint;
} // 优化擦除逻辑
private void ErasePaths()
{
// 倒序遍历所有路径
for (int i = Paths.Count - 1; i >= 0; i--)
{
var path = Paths[i]; // 检查橡皮擦轨迹上的所有点
foreach (var trailPoint in eraserTrail)
{
if (path.IntersectAt(trailPoint, EraserRadius))
{
var newPaths = path.SplitAt(trailPoint, EraserRadius); if (newPaths.Count > 0)
{
Paths.RemoveAt(i);
foreach (var newPath in newPaths)
{
if (newPath.Path.Count >= 2) // 只添加有效路径
{
Paths.Add(newPath);
}
}
break; // 路径已被处理,跳出循环
}
else
{
// 没有新路径表示整个路径应被删除
Paths.RemoveAt(i);
break;
}
}
}
}
} private void MoveSelectedPaths(PointF currentPoint)
{
float deltaX = currentPoint.X - lastTouchPoint.X;
float deltaY = currentPoint.Y - lastTouchPoint.Y; foreach (var path in Paths)
{
if (path.IsSelected)
{
path.Pos = new PointF(path.Pos.X + deltaX, path.Pos.Y + deltaY);
path.InvalidateBounds();
}
}
lastTouchPoint = currentPoint;
} private void UpdateSelectionRect(PointF currentPoint)
{
float x = Math.Min(lastTouchPoint.X, currentPoint.X);
float y = Math.Min(lastTouchPoint.Y, currentPoint.Y);
float width = Math.Abs(currentPoint.X - lastTouchPoint.X);
float height = Math.Abs(currentPoint.Y - lastTouchPoint.Y); selectionRect = new RectF(x, y, width, height);
} private void OnTouchEnded(object? sender, TouchEventArgs e)
{
switch (EditingMode)
{
case InkCanvasEditingMode.Select when selectionRect.HasValue:
FinalizeSelection();
break;
} currentPath = null;
selectionRect = null;
isMovingSelection = false;
Eraser.IsSelected = false;
eraserTrail.Clear(); // 清除橡皮擦轨迹
Invalidate();
} private void FinalizeSelection()
{
var selection = selectionRect!.Value; foreach (var path in Paths)
{
if (!selection.IntersectsWith(path.Bounds)) continue; if (selection.Contains(path.Bounds))
{
path.IsSelected = true;
continue;
} foreach (var point in path.Path.Points)
{
var absolutePoint = new PointF(point.X + path.Pos.X, point.Y + path.Pos.Y);
if (selection.Contains(absolutePoint))
{
path.IsSelected = true;
break;
}
}
}
} public void ClearSelection()
{
foreach (var path in Paths)
{
path.IsSelected = false;
}
} public void Draw(ICanvas canvas, RectF dirtyRect)
{
canvas.FillColor = BackgroundColor;
canvas.FillRectangle(dirtyRect); canvas.StrokeLineCap = LineCap.Round;
canvas.StrokeLineJoin = LineJoin.Round; // 绘制所有路径
foreach (var path in Paths)
{
// 跳过无效路径(少于2个点)
if (path.Path.Count < 2) continue; DrawPath(canvas, path);
} // 绘制橡皮擦(如果被选中)
if (Eraser.IsSelected)
{
DrawEraser(canvas);
} // 绘制选择框
if (selectionRect.HasValue)
{
DrawSelectionRect(canvas, selectionRect.Value);
}
} private void DrawPath(ICanvas canvas, DrawingPath path)
{
var strokeColor = path.StrokeColor ?? Colors.Black;
float strokeSize = path.IsSelected ? path.StrokeThickness * 1.5f : path.StrokeThickness; if (!path.IsSelected)
{
strokeColor = strokeColor.WithAlpha(0.5f);
} canvas.StrokeColor = strokeColor;
canvas.StrokeSize = strokeSize; canvas.SaveState();
canvas.Translate(path.Pos.X, path.Pos.Y);
canvas.DrawPath(path.Path);
canvas.RestoreState();
} private void DrawEraser(ICanvas canvas)
{
canvas.SaveState();
canvas.Translate(Eraser.Pos.X, Eraser.Pos.Y);
canvas.Scale(0.2f, 0.2f);
canvas.StrokeColor = Eraser.StrokeColor ?? Colors.Black;
canvas.StrokeSize = Eraser.StrokeThickness;
canvas.FillColor = Color.FromArgb("#FFD700");
canvas.FillPath(Eraser.Path);
canvas.DrawPath(Eraser.Path);
canvas.RestoreState();
} private void DrawSelectionRect(ICanvas canvas, RectF rect)
{
canvas.SaveState();
canvas.StrokeColor = SelectionColor;
canvas.StrokeSize = SelectionStrokeThickness;
canvas.StrokeDashPattern = new float[] { 5, 3 };
canvas.DrawRectangle(rect);
canvas.RestoreState();
} // 静态工具方法
public static float Distance(PointF a, PointF b)
=> (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2)); public static float DistanceSquared(PointF a, PointF b)
=> (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y); public static float PointToLineDistance(PointF lineStart, PointF lineEnd, PointF point)
{
float l2 = DistanceSquared(lineStart, lineEnd);
if (l2 == 0) return Distance(point, lineStart); float t = Math.Max(0, Math.Min(1, Vector2.Dot(
new Vector2(point.X - lineStart.X, point.Y - lineStart.Y),
new Vector2(lineEnd.X - lineStart.X, lineEnd.Y - lineStart.Y)) / l2)); PointF projection = new PointF(
lineStart.X + t * (lineEnd.X - lineStart.X),
lineStart.Y + t * (lineEnd.Y - lineStart.Y)
); return Distance(point, projection);
}
}
SimpleInkCanvas.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Shares.Utility;assembly=Shares"
x:Class="MauiViews.MauiDemos.Book._03.SimpleInkCanvas"
Title="SimpleInkCanvas" HeightRequest="300" WidthRequest="300">
<Grid RowDefinitions="auto,*">
<StackLayout Margin="5" Orientation="Horizontal">
<Label Text="EditingMode:" Margin="5" VerticalOptions="Center" FontSize="16"/>
<Picker x:Name="lstEditingMode" VerticalOptions="Center"/>
</StackLayout>
<local:InkCanvas Grid.Row="1" BackgroundColor="LightYellow"
EditingMode="{Binding Path=SelectedIndex,
Source={x:Reference lstEditingMode}, Converter={StaticResource Converter}}"/>
<Button Text="Hello" Grid.Row="1" WidthRequest="78" HeightRequest="16"
HorizontalOptions="Start" VerticalOptions="Start"/>
</Grid>
</ContentPage>
对应的cs代码
using static Shares.Utility.InkCanvas; namespace MauiViews.MauiDemos.Book._03; public partial class SimpleInkCanvas : ContentPage
{
public SimpleInkCanvas()
{
InitializeComponent();
foreach (InkCanvasEditingMode mode in Enum.GetValues(typeof(InkCanvasEditingMode)))
{
lstEditingMode.Items.Add(mode.ToString());
lstEditingMode.SelectedIndex = 0;
}
}
}
运行效果

03 - LayoutPanels例子 - SimpleInkCanvas的更多相关文章
- day23 03 组合的例子
day23 03 组合的例子 一.用到组合的方式,编写一个圆环,并能够计算出它的周长和面积 from math import pi # 从内置函数里面导入pi # 先定义一个圆类 class Circ ...
- python 各模块
01 关于本书 02 代码约定 03 关于例子 04 如何联系我们 1 核心模块 11 介绍 111 内建函数和异常 112 操作系统接口模块 113 类型支持模块 114 正则表达式 115 语言支 ...
- c socket(续)
存在两种字节顺序:NBO与HBO 网络字节顺序NBO(Network Byte Order):按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题. 主机字节顺序(HBO,Host ...
- 编程那些事儿:如何快速地"借用"CSS
做前端开发有时候会碰到任务紧急,需要马上写好静态页的问题.比如,设计师给你扔了一个设计稿,要你在下班之前搞定.这时候你如热锅上的蚂蚁,如果自己写css的话,时间紧张,于是上网找了一下相关模板页面,找到 ...
- urls.py的配置[路由配置]
urls.py的配置[路由配置] Get请求与Post请求的方式 get请求: (1)地址栏输入url (2)<a href="请求url">点击</a> ...
- 【Python】【自动化测试】【pytest】
https://docs.pytest.org/en/latest/getting-started.html#create-your-first-test http://www.testclass.n ...
- c# 主机和网络字节序的转换 关于网络字节序和主机字节序的转换
最近使用C#进行网络开发,需要处理ISO8583报文,由于其中有些域是数值型的,于是在传输的时候涉及到了字节序的转换. 字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有两种字节顺序, ...
- [C++][转]CPU字节序 网络序 主机序 大端小端
原帖:http://www.cnblogs.com/darktime/p/3298075.html 不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序最常见的有两种1 ...
- Python:数字的格式化输出
>>> 'The value is {:0,.2f}'.format(x) 'The value is 1,234.57' 需要将数字格式化后输出,并控制数字的位数.对齐.千位分隔符 ...
- [Python3] 027 常用模块 time
目录 time 1. 时间戳 2. UTC 时间 3. 夏令时 4. 时间元组 5. 举例 5.1 例子1 例子2 例子3 例子4 例子5 例子6 例子7 time 1. 时间戳 一个时间表示,根据不 ...
随机推荐
- IvorySQL v4 逻辑复制槽同步功能解析:高可用场景下的数据连续性保障
功能简介 IvorySQL v4 基于 PostgreSQL 17,引入了逻辑复制槽同步至热备份数据库的功能.这一改进有效解决了旧版本中主数据库与备份数据库切换后逻辑复制中断的问题.对于那些追求数据高 ...
- Linux系统查看CPU使用率、内存使用率、磁盘使用率
一.查看CPU使用率1. top 命令[root@sss ~]# toptop - 16:54:38 up 7 days, 5:13, 3 users, load average: 0.00, ...
- 为什么不建议通过Executors构建线程池
Executors类看起来功能还是比较强大的,又用到了工厂模式.又有比较强的扩展性,重要的是用起来还比较方便,如: ExecutorService executor = Executors.newFi ...
- 一文速通Python并行计算:04 Python多线程编程-多线程同步(上)—基于条件变量、事件和屏障
一文速通 Python 并行计算:04 Python 多线程编程-多线程同步(下)-基于条件变量.事件和屏障 摘要: 本文介绍了 Python 多线程同步的三种机制:条件变量(Condition).事 ...
- centos7 下全局配置最新版的golang语言开发环境
按照以下步骤进行操作: 前往Go官方网站下载页面(https://golang.org/dl/)查找最新版本的Go二进制文件. 使用wget命令下载最新版本的Go二进制文件.例如,如果最新版本是1.1 ...
- 支持VS2022的包发布工具NuPack 2022发布
我们很高兴地宣布 NuPack 2022 正式发布!这是一个开源项目,旨在简化 .NET 开发中的 NuGet 包发布流程. NuPack 是什么? NuPack 是一个轻量级工具,VS扩展,它可以帮 ...
- 【SpringCloud】SpringCloud Sleuth分布式链路跟踪
SpringCloud Sleuth分布式链路跟踪 概述 为什么会出现这个技术?需要解决哪些问题? 问题:在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后 ...
- ASP.NET 简单实现数字时钟
每隔1000毫秒(1s)获取一次当前时间 <asp:ScriptManager ID="ScriptManager_Time" runat="server" ...
- Devops相关考试和认证
Devops相关考试和认证 Red Hat Certified System Administrator (RHCSA) 能够执行以下任务: 了解和运用必要的工具来处理文件.目录.命令行环境和文档 操 ...
- 将Particle转成UGUI
在unity官方论坛看到的一个解决方案,可以将Particle直接转换成CanvasRenderer元素显示.新建一个UIParticleSystem.cs脚本,将以下代码复制进去: using Un ...