原文:WPF 自定义的图表(适用大量数据绘制)下

上一篇文章中讲了WPF中自定义绘制大量数据的图标,思路是先将其绘制在内存,然后一次性加载到界面,在后续的调试过程中,发现当数据量到达10W时,移动鼠标显示数据有明显的延迟。经过思考,我采用了以下两个办法解决这个问题:
1.将数据显示的文本与图表分离,作为一个单独的canvas,这样,显示文本数据的时候就不需要重画图表了
2.计算鼠标移动速度,当移动速度过快时,不绘制文本,减少数据文本的绘制频率
3.使用START_INDEX 和 END_INDEX来表示绘制数据的其实位置和结束位置,减少对整个数据的遍历

首先看前台代码
<UserControl x:Class="Zero_Gjy.UserControls.DrawChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Zero_Gjy.UserControls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ScrollViewer > <Grid x:Name="linechart" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> </Grid>
</ScrollViewer>
</UserControl>

在lineChart中分别加入图表的Canvas和文本数据的Canvas,修改之前的DrawingCanvas,把绘制数据文本的代码分离出来

DrawingCanvas.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading; namespace Zero_Gjy.UserControls
{
public class DrawingCanvas : Canvas
{
private List<Visual> visuals = new List<Visual>(); public const double YZero = 50;
public const double YMargin = 50;
private double YLabelLen = 5;
private double yHeight;
private Brush line1Color = Brushes.Black;
private Brush labelColor = Brushes.Black;
private Brush axisColor = Brushes.Black;
private Brush line2Color = Brushes.Black;
private double thinkness = 1;
private double canvasWidth = 0;
private double xWidth;
private const int yLinesCount = 12; List<LineDatas> allDatas;
private double canvasHeight;
int dataCount = 0;
int rtCount = 0;
public enum MouseMode
{
ZOOM,
VIEW
}
public MouseMode mMode = MouseMode.VIEW;
public DrawingCanvas()
{
this.Background = Brushes.Transparent;
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
this.Margin = new Thickness(0, 0, 0, YMargin);
AllDatas = new List<LineDatas>(); } //获取Visual的个数
protected override int VisualChildrenCount
{
get { return visuals.Count; }
} //获取Visual
protected override Visual GetVisualChild(int index)
{
return visuals[index];
} //添加Visual
public void AddVisual(Visual visual)
{
visuals.Add(visual); base.AddVisualChild(visual);
base.AddLogicalChild(visual);
} //删除Visual
public void RemoveVisual(Visual visual)
{
visuals.Remove(visual); base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
} //命中测试
public DrawingVisual GetVisual(Point point)
{
HitTestResult hitResult = VisualTreeHelper.HitTest(this, point);
return hitResult.VisualHit as DrawingVisual;
} //使用DrawVisual画Polyline
//DrawingVisual lineVisual;
public void addData(string title,double min,double max,DoubleCollection xData, DoubleCollection mData, DoubleCollection sData)
{
AllDatas.Add(new LineDatas(title, min, max, xData,mData, sData)); dataCount++;
rtCount = mData.Count;
}
public void removeFirst() {
for(int i = 0; i < AllDatas.Count; i++)
{
AllDatas[i].RtData.RemoveAt(0);
AllDatas[i].ThData.RemoveAt(0);
}
}
public void removeLast()
{
for (int i = 0; i < AllDatas.Count; i++)
{
AllDatas[i].RtData.RemoveAt(AllDatas[i].RtData.Count - 1);
AllDatas[i].ThData.RemoveAt(AllDatas[i].ThData.Count - 1);
}
}
/// <summary>
/// 添加数据点
/// </summary>
/// <param name="xdatas">x坐标</param>
/// <param name="rtdatas">实测数据</param>
/// <param name="thdatas">理论数据</param>
public void addPoint(double[] xdatas,double[] rtdatas,double[] thdatas)
{
if (rtdatas.Length != allDatas.Count)
throw new Exception() { Source="数据个数不匹配"};
for (int i = 0; i < allDatas.Count; i++)
{
AllDatas[i].XData.Insert(0, xdatas[i]);
AllDatas[i].RtData.Insert(0,rtdatas[i]);
AllDatas[i].ThData.Insert(0,thdatas[i]);
if (allDatas[i].Min > rtdatas[i])
allDatas[i].Min = rtdatas[i];
if (allDatas[i].Max < rtdatas[i])
allDatas[i].Max = rtdatas[i];
}
}
//清空所有数据
public void clearData()
{
AllDatas.Clear(); }
//将数据清零
public void cleanData()
{
foreach(LineDatas item in allDatas)
{
for(int i = 0; i < item.RtData.Count; i++)
{
item.RtData[i] = 0;
item.ThData[i] = 0;
}
item.Min = -1;
item.Max = 1;
}
}
public void Polyline()
{
//如果虚画布没有数据
if(this.visuals.Count == 0)
{
for(int i = 0; i < AllDatas.Count; i++)
{
this.visuals.Add(new DrawingVisual());
}
}
for(int count = 0; count < AllDatas.Count; count++){
//计算基础坐标系
double x0, y0;
x0 = YZero;
y0 = (count + 1) * YMargin + count * yHeight;
string title = AllDatas[count].Title; LineDatas datas = allDatas[count];
DrawingVisual lineVisual = (DrawingVisual)this.visuals[count];
DrawingContext dc = lineVisual.RenderOpen();
Pen penAxis = new Pen(AxisColor, Thinkness);
penAxis.Freeze(); int yLabelMax = (int)AllDatas[count].Max + 1;
int yLabelMin = (int)AllDatas[count].Min - 1;
//画标题
FormattedText fttitle = new FormattedText(title, new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 15, Brushes.Black);
dc.DrawText(fttitle, new Point(xWidth / 2+x0 - fttitle.Width / 2, y0 - fttitle.Height));
//画Y轴
dc.DrawLine(penAxis, new Point(x0, y0), new Point(x0, y0 + yHeight));
dc.DrawLine(penAxis, new Point(x0 - YLabelLen, y0), new Point(x0, y0));
dc.DrawLine(penAxis, new Point(x0 - YLabelLen, y0 + yHeight / 2), new Point(x0, y0 + yHeight / 2));
dc.DrawLine(penAxis, new Point(x0 - YLabelLen, y0 + yHeight), new Point(x0, y0 + yHeight));
//y轴文本
FormattedText ft1 = new FormattedText(yLabelMax.ToString(), new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 10, Brushes.Black);
dc.DrawText(ft1, new Point(x0 - YLabelLen - ft1.Width, y0 - ft1.Height / 2));
FormattedText ft2 = new FormattedText(((yLabelMax + yLabelMin) / 2).ToString(), new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 10, Brushes.Black);
dc.DrawText(ft2, new Point(x0 - YLabelLen - ft2.Width, y0 + YHeight / 2 - ft2.Height / 2));
FormattedText ft3 = new FormattedText(yLabelMin.ToString(), new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 10, Brushes.Black);
dc.DrawText(ft3, new Point(x0 - YLabelLen - ft3.Width, y0 + yHeight - ft3.Height / 2));
//画线
Pen linePen1 = new Pen(Line1Color, Thinkness);
linePen1.Freeze();
Pen linePen2 = new Pen(Line2Color, Thinkness);
linePen2.Freeze();
double ratio = (yLabelMax - yLabelMin) / yHeight;
double step = xWidth / (datas.EndIndex - datas.StartIndex +1);
//Console.WriteLine("=======datastep:" + step);
for (int i = datas.StartIndex; i < datas.EndIndex; i++)
{
//将数值转换成位置
//理论值
Point p3 = new Point(x0 + (i - datas.StartIndex)*step, y0 + (yLabelMax - datas.ThData[i]) / ratio);
Point p4 = new Point(x0 + (i - datas.StartIndex + 1)*step, y0 + (yLabelMax - datas.ThData[i + 1]) / ratio);
dc.DrawLine(linePen2, p3, p4);
//实测值
Point p1 = new Point(x0 +(i - datas.StartIndex) *step, y0 + (yLabelMax - datas.RtData[i]) / ratio);
Point p2 = new Point(x0 + (i - datas.StartIndex + 1)*step, y0 + (yLabelMax - datas.RtData[i + 1]) / ratio);
dc.DrawLine(linePen1, p1, p2);
}
//绘制纵向网格和x轴文本
Pen pen3 = new Pen(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#666666")), 1);
pen3.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0);
pen3.Freeze();
double stepX = xWidth / yLinesCount;
int stepData = (datas.EndIndex - datas.StartIndex + 1) / yLinesCount;
if (stepData < 1)
stepData = 1;
for (int i = 1; i < yLinesCount; i++)
{
//纵向网格
Point p1 = new Point(x0 + i * stepX, y0 + yHeight);
Point p2 = new Point(x0 + i * stepX, y0);
dc.DrawLine(pen3, p1, p2);
//x轴文本
FormattedText ftX = new FormattedText(datas.XData[i * stepData].ToString(), new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 10, Brushes.Black);
Point p3 = new Point(x0 + i * stepX - ftX.Width / 2, yHeight + y0);
dc.DrawText(ftX, p3);
}
dc.Close();
}
this.InvalidateVisual();
} public double YHeight
{
get
{
return yHeight;
} set
{
yHeight = value;
}
} public Brush LabelColor
{
get
{
return labelColor;
} set
{
labelColor = value;
}
} public Brush AxisColor
{
get
{
return axisColor;
} set
{
axisColor = value;
}
} public double Thinkness
{
get
{
return thinkness;
} set
{
thinkness = value;
}
} public static double YZero1
{
get
{
return YZero;
}
} public double CanvasWidth
{
get
{
return canvasWidth;
} set
{
canvasWidth = value;
this.Width = value;
xWidth = value - YMargin - YZero;
}
} public Brush Line2Color
{
get
{
return line2Color;
} set
{
line2Color = value;
}
} public Brush Line1Color
{
get
{
return line1Color;
} set
{
line1Color = value;
}
} public double CanvasHeight
{
get
{
return canvasHeight;
} set
{
canvasHeight = value;
}
} public int DataCount
{
get
{
return dataCount;
} set
{
dataCount = value;
}
} public int RtCount
{
get
{
return rtCount;
} set
{
rtCount = value;
}
} internal List<LineDatas> AllDatas
{
get
{
return allDatas;
} set
{
allDatas = value;
}
}
}
}

DrawingLine.CS

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading; namespace Zero_Gjy.UserControls
{
class DrawingLine : Canvas
{
private List<Visual> visuals = new List<Visual>();
DrawingCanvas drawingCanvas;
private double x0;
private double y0;
private double mWidth;
private double mHeight;
public enum MouseMode
{
ZOOM,
VIEW
}
public MouseMode mMode = MouseMode.VIEW;
private double canvasWidth;
public DrawingLine(DrawingCanvas _dc)
{
this.drawingCanvas = _dc;
this.Background = Brushes.Transparent;
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
this.Height = drawingCanvas.CanvasHeight;
this.Width = drawingCanvas.CanvasWidth; x0 = DrawingCanvas.YZero;
y0 = DrawingCanvas.YMargin; mHeight = _dc.AllDatas.Count * (DrawingCanvas.YMargin + _dc.YHeight); this.PreviewMouseLeftButtonDown += DrawingCanvas_MouseLeftButtonDown;
this.PreviewMouseLeftButtonUp += DrawingCanvas_MouseLeftButtonUp;
this.MouseMove += DrawingCanvas_MouseMove;
this.MouseLeave += DrawingCanvas_MouseLeave;
}
//鼠标离开画布
private void DrawingCanvas_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
if (mMode == MouseMode.VIEW)
{
this.RemoveVisual(textVisual);
this.InvalidateVisual();
}
}
//选中放大区域完成,显示放大区域
private void DrawingCanvas_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
isMouseDown = false;
mMode = MouseMode.VIEW;
endup = e.GetPosition(this).X;
if (endup < x0)
endup = x0;
if (endup > x0+mWidth)
endup = x0 + mWidth;
//重画 选中区域
int len = drawingCanvas.AllDatas[0].EndIndex - drawingCanvas.AllDatas[0].StartIndex+1;
if (drawingCanvas.AllDatas.Count < 1 || len < 1)
return;
double step = mWidth / len;
if (isMouseMoved && Math.Abs(endup - startDown) > step)
{
int startIndex = 0;
int endIndex = 0;
if (endup > startDown)
{
startIndex = (int)((startDown - x0) / step) + 1;
endIndex = (int)((endup - x0) / step);
}
else
{
startIndex = (int)((endup - x0) / step) + 1;
endIndex = (int)((startDown - x0) / step);
} foreach (LineDatas data in drawingCanvas.AllDatas)
{
data.EndIndex = data.StartIndex + endIndex;
data.StartIndex = data.StartIndex + startIndex;
}
drawingCanvas.Polyline();
this.RemoveVisual(rectVisual);
this.InvalidateVisual();
}
isMouseMoved = false;
}
double preMoved = DrawingCanvas.YZero;
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
//long preTime = 0;
private const long MIN_TIME= 15000;
private void DrawingCanvas_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{ double positionX = e.GetPosition(this).X;
if (mMode == MouseMode.VIEW)
{
int len = drawingCanvas.AllDatas[0].EndIndex - drawingCanvas.AllDatas[0].StartIndex+1;
if (positionX > x0 && positionX < x0+mWidth && Math.Abs(positionX - preMoved) >= mWidth / len)
{
//计算速度鼠标移动速度,如果速度过快 ,则不绘制
if (stopwatch.IsRunning)
{
stopwatch.Stop();
}
long curTime = stopwatch.ElapsedTicks;
//Console.WriteLine(curTime);
if(curTime > MIN_TIME)
{
this.PolyText(positionX);
}
stopwatch.Restart();
preMoved = positionX; }
}
else if (isMouseDown && startDown > x0 && startDown < mWidth+x0)
{
isMouseMoved = true;
this.PolyRect(startDown, positionX);
} //stopwatch.Start();
} private bool isMouseDown = false;
private bool isMouseMoved = false;
private double startDown;
private double endup;
private int clickTimes = 0;
private void DrawingCanvas_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{ isMouseDown = true;
mMode = MouseMode.ZOOM;
startDown = e.GetPosition(this).X;
this.RemoveVisual(textVisual);
this.InvalidateVisual();
//双击
clickTimes++;
DispatcherTimer timer = new DispatcherTimer(); timer.Interval = new TimeSpan(0, 0, 0, 0, 300); timer.Tick += (s, e1) => { timer.IsEnabled = false; clickTimes = 0; }; timer.IsEnabled = true; if (clickTimes % 2 == 0)
{
//Console.WriteLine("double");
timer.IsEnabled = false; clickTimes = 0;
foreach (LineDatas data in drawingCanvas.AllDatas)
{
data.StartIndex = 0;
data.EndIndex = data.RtData.Count - 1;
} drawingCanvas.Polyline();
drawingCanvas.InvalidateVisual();
//this.InvalidateVisual();
}
} //获取Visual的个数
protected override int VisualChildrenCount
{
get { return visuals.Count; }
} public double MWidth
{
get
{
return mWidth;
} set
{
mWidth = value;
}
} public double MHeight
{
get
{
return mHeight;
} set
{
mHeight = value;
}
} public double CanvasWidth
{
get
{
return canvasWidth;
} set
{
canvasWidth = value;
this.Width = value;
mWidth = value - x0 - y0;
}
} //获取Visual
protected override Visual GetVisualChild(int index)
{
return visuals[index];
} //添加Visual
public void AddVisual(Visual visual)
{
visuals.Add(visual); base.AddVisualChild(visual);
base.AddLogicalChild(visual);
} //删除Visual
public void RemoveVisual(Visual visual)
{
visuals.Remove(visual); base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
//visual = null;
} //命中测试
public DrawingVisual GetVisual(Point point)
{
HitTestResult hitResult = VisualTreeHelper.HitTest(this, point);
return hitResult.VisualHit as DrawingVisual;
} //绘制鼠标选择放大的矩形
DrawingVisual rectVisual;
public void PolyRect(double startx, double endx)
{
if (rectVisual != null)
{
this.RemoveVisual(rectVisual);
}
rectVisual = new DrawingVisual();
DrawingContext dc = rectVisual.RenderOpen(); Pen pen = new Pen(new SolidColorBrush(Color.FromArgb(100, 255, 200, 200)), 1);
dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(100, 255, 200, 200)), pen, new Rect(new Point(startx, y0), new Point(endx, y0+mHeight)));
dc.Close();
this.AddVisual(rectVisual);
this.InvalidateVisual();
}
//绘制显示选中的数值
DrawingVisual textVisual;
public void PolyText(double xPosition)
{
if (drawingCanvas.AllDatas.Count < 1)
return;
if (textVisual != null)
{
this.RemoveVisual(textVisual);
}
textVisual = new DrawingVisual(); DrawingContext dc = textVisual.RenderOpen();
int len = drawingCanvas.AllDatas[0].EndIndex - drawingCanvas.AllDatas[0].StartIndex+1;
double step = mWidth / len;
// Console.WriteLine("*****linestep:" + step);
int index = (int)((xPosition - x0) / step);
double ax = x0 + index * step;
//竖线
Pen pen = new Pen(Brushes.Transparent, 3);
pen.Freeze();
FormattedText[] ftXs = new FormattedText[drawingCanvas.AllDatas.Count]; for (int i = 0; i < drawingCanvas.AllDatas.Count; i++)
{
int mIndex = index + drawingCanvas.AllDatas[i].StartIndex;
ftXs[i] = new FormattedText("X:" + drawingCanvas.AllDatas[i].XData[mIndex] + " 实测:" + drawingCanvas.AllDatas[i].RtData[mIndex] + " 设计:" + drawingCanvas.AllDatas[i].ThData[mIndex], new System.Globalization.CultureInfo("zh-CHS", false), FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 15, Brushes.White);
//计算是否超出范围
if (ax + ftXs[i].Width < x0+mWidth)
{
dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(200, 0, 150, 179)), pen, new Rect(new Point(ax, (i+1)*y0+i*drawingCanvas.YHeight), new Point(ax + ftXs[i].Width, (i + 1) * y0 + i * drawingCanvas.YHeight + ftXs[i].Height)));
dc.DrawText(ftXs[i], new Point(ax, (i + 1) * y0 + i * drawingCanvas.YHeight));
}
else
{
dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(200, 0, 150, 179)), pen, new Rect(new Point(ax - ftXs[i].Width, (i + 1) * y0 + i * drawingCanvas.YHeight), new Point(ax, (i + 1) * y0 + i * drawingCanvas.YHeight + ftXs[i].Height)));
dc.DrawText(ftXs[i], new Point(ax - ftXs[i].Width, (i + 1) * y0 + i * drawingCanvas.YHeight));
}
}
Pen penLine = new Pen(new SolidColorBrush(Color.FromArgb(200, 0, 150, 179)), 3);
penLine.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0);
penLine.Freeze();
dc.DrawLine(penLine, new Point(ax,y0), new Point(ax, y0+mHeight));
dc.Close();
this.AddVisual(textVisual);
this.InvalidateVisual();
} }
}

上面两段代码中,我把需要绘制的数据属性单独抽象出来

LineDatas.CS

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media; namespace Zero_Gjy.UserControls
{
class LineDatas
{
string title;
double min;
double max;
DoubleCollection xData;
DoubleCollection rtData;
DoubleCollection thData;
int startIndex;
int endIndex;
public double Min
{
get
{
return min;
} set
{
min = value;
}
} public double Max
{
get
{
return max;
} set
{
max = value;
}
} public string Title
{
get
{
return title;
} set
{
title = value;
}
} public DoubleCollection RtData
{
get
{
return rtData;
} set
{
rtData = value;
min = value.Min();
max = value.Max();
}
} public DoubleCollection ThData
{
get
{
return thData;
} set
{
thData = value; }
} public int StartIndex
{
get
{
return startIndex;
} set
{
startIndex = value;
}
} public int EndIndex
{
get
{
return endIndex;
} set
{
endIndex = value;
}
} public DoubleCollection XData
{
get
{
return xData;
} set
{
xData = value;
}
} public LineDatas()
{
this.title = "";
this.Max = 0;
this.Min = 0;
this.XData = new DoubleCollection();
this.RtData = new DoubleCollection();
this.ThData = new DoubleCollection(); }
/// <summary>
///
/// </summary>
/// <param name="_title">数据标题</param>
/// <param name="min">数据最小值</param>
/// <param name="max">数据最大值</param>
/// <param name="_xData">x坐标</param>
/// <param name="rtdata">实测数据</param>
/// <param name="thdata">理论数据</param>
public LineDatas(string _title,double min, double max, DoubleCollection _xData, DoubleCollection rtdata,DoubleCollection thdata)
{
this.title = _title;
this.Min = min;
this.Max = max;
this.XData = _xData;
this.RtData = rtdata ;
this.ThData = thdata;
this.StartIndex = 0;
this.EndIndex = rtData.Count - 1;
}
public void clear()
{
this.XData.Clear();
this.RtData.Clear();
this.ThData.Clear();
}
}
}

经过这样的改进,鼠标在界面移动时显示数据明显顺畅了很多。



最新文章

  1. golang语言构造函数
  2. Objective-C runtime初识
  3. Linux平台oracle 11g单实例 安装部署配置 快速参考
  4. 窗体作为控件嵌入panel
  5. 一个美术需求引发的Custom Inspector
  6. 【BZOJ2186】【SDoi2008】沙拉公主的困惑 数论
  7. 在Linux中使用vi打开文件时如何显示行号,及跳转到指定行
  8. Ubuntu 12.04 卸载 VMware
  9. SDIO接口
  10. Java快速排序
  11. Spring 基于注解的装配
  12. PHP学习资源
  13. 全景智慧城市常诚——没接触过VR全景的你就是目前VR最大的新闻
  14. Hibernate入门(五)
  15. 快排实现仿order by多字段排序
  16. 【安卓进阶】Scroller理解与应用
  17. sjms-3 结构型模式
  18. canvas加载图片需要二次刷新的问题
  19. 《Spring5官方文档》新功能(4,3)
  20. 微信小程序常见问题

热门文章

  1. [CSS] Draw Simple Icons with CSS
  2. [Ramda] allPass, propEq
  3. MS SQL Server的STRING_SPLIT和STRING_AGG函数
  4. 【机器学习实战】第4章 朴素贝叶斯(Naive Bayes)
  5. [CSS Flex] Justify-content
  6. iOS 项目的文件夹结构能看出你的开发经验
  7. iOS中html5的交互:WebViewJavascriptBridge
  8. erlang抽象码与basho的protobuf
  9. Wpf的布局舍入属性(可以解决软件字体模糊的问题)
  10. 从张量积(tensor product)到多重线性代数(multilinear algebra)