查看: 2167|回复: 0

[.NET开发] WPF图形解锁控件ScreenUnLock使用详解

发表于 2018-1-2 08:00:00

ScreenUnLock 与智能手机上的图案解锁功能一样。通过绘制图形达到解锁或记忆图形的目的。

本人突发奇想,把手机上的图形解锁功能移植到WPF中。也应用到了公司的项目中。

在创建ScreenUnLock之前,先来分析一下图形解锁的实现思路。

1.创建九宫格原点(或更多格子),每个点定义一个坐标值

2.提供图形解锁相关扩展属性和事件,方便调用者定义。比如:点和线的颜色(Color),操作模式(Check|Remember),验证正确的颜色(RightColor), 验证失败的颜色(ErrorColor), 解锁事件 OnCheckedPoint,记忆事件 OnRememberPoint 等;

3.定义MouseMove事件监听画线行为。 画线部分也是本文的核心。在画线过程中。程序需判断,线条从哪个点开始绘制,经过了哪个点(排除已经记录的点)。是否完成了绘制等等。

4.画线完成,根据操作模式处理画线完成行为。并调用相关自定义事件

大致思路如上,下面开始一步一步编写ScreenUnLock吧

创建ScreenUnLock

  1. public partial class ScreenUnlock : UserControl
复制代码

定义相关属性

  1. /// <summary>
  2. /// 验证正确的颜色
  3. /// </summary>
  4. private SolidColorBrush rightColor;
  5. /// <summary>
  6. /// 验证失败的颜色
  7. /// </summary>
  8. private SolidColorBrush errorColor;
  9. /// <summary>
  10. /// 图案是否在检查中
  11. /// </summary>
  12. private bool isChecking;
  13. public static readonly DependencyProperty PointArrayProperty = DependencyProperty.Register("PointArray", typeof(IList<string>), typeof(ScreenUnlock));
  14. /// <summary>
  15. /// 记忆的坐标点
  16. /// </summary>
  17. public IList<string> PointArray
  18. {
  19. get { return GetValue(PointArrayProperty) as IList<string>; }
  20. set { SetValue(PointArrayProperty, value); }
  21. }
  22. /// <summary>
  23. /// 当前坐标点集合
  24. /// </summary>
  25. private IList<string> currentPointArray;
  26. /// <summary>
  27. /// 当前线集合
  28. /// </summary>
  29. private IList<Line> currentLineList;
  30. /// <summary>
  31. /// 点集合
  32. /// </summary>
  33. private IList<Ellipse> ellipseList;
  34. /// <summary>
  35. /// 当前正在绘制的线
  36. /// </summary>
  37. private Line currentLine;
  38. public static readonly DependencyProperty OperationPorperty = DependencyProperty.Register("Operation", typeof(ScreenUnLockOperationType), typeof(ScreenUnlock), new FrameworkPropertyMetadata(ScreenUnLockOperationType.Remember));
  39. /// <summary>
  40. /// 操作类型
  41. /// </summary>
  42. public ScreenUnLockOperationType Operation
  43. {
  44. get { return (ScreenUnLockOperationType)GetValue(OperationPorperty); }
  45. set { SetValue(OperationPorperty, value); }
  46. }
  47. public static readonly DependencyProperty PointSizeProperty = DependencyProperty.Register("PointSize", typeof(double), typeof(ScreenUnlock), new FrameworkPropertyMetadata(15.0));
  48. /// <summary>
  49. /// 坐标点大小
  50. /// </summary>
  51. public double PointSize
  52. {
  53. get { return Convert.ToDouble(GetValue(PointSizeProperty)); }
  54. set { SetValue(PointSizeProperty, value); }
  55. }
  56. public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(SolidColorBrush), typeof(ScreenUnlock), new FrameworkPropertyMetadata(new SolidColorBrush(Colors.White), new PropertyChangedCallback((s, e) =>
  57. {
  58. (s as ScreenUnlock).Refresh();
  59. })));
  60. /// <summary>
  61. /// 坐标点及线条颜色
  62. /// </summary>
  63. public SolidColorBrush Color
  64. {
  65. get { return GetValue(ColorProperty) as SolidColorBrush; }
  66. set { SetValue(ColorProperty, value); }
  67. }
  68.     /// <summary>
  69.     /// 操作类型
  70.     /// </summary>
  71.     public enum ScreenUnLockOperationType
  72.     {
  73.     Remember = 0, Check = 1
  74.     }
复制代码

初始化ScreenUnLock

  1. public ScreenUnlock()
  2. {
  3. InitializeComponent();
  4. this.Loaded += ScreenUnlock_Loaded;
  5. this.Unloaded += ScreenUnlock_Unloaded;
  6. this.MouseMove += ScreenUnlock_MouseMove; //监听绘制事件
  7. }
  8. private void ScreenUnlock_Loaded(object sender, RoutedEventArgs e)
  9. {
  10. isChecking = false;
  11. rightColor = new SolidColorBrush(Colors.Green);
  12. errorColor = new SolidColorBrush(Colors.Red);
  13. currentPointArray = new List<string>();
  14. currentLineList = new List<Line>();
  15. ellipseList = new List<Ellipse>();
  16. CreatePoint();
  17. }
  18. private void ScreenUnlock_Unloaded(object sender, RoutedEventArgs e)
  19. {
  20. rightColor = null;
  21. errorColor = null;
  22. if (currentPointArray != null)
  23. this.currentPointArray.Clear();
  24. if (currentLineList != null)
  25. this.currentLineList.Clear();
  26. if (ellipseList != null)
  27. ellipseList.Clear();
  28. this.canvasRoot.Children.Clear();
  29. }
复制代码

创建点

  1. /// <summary>
  2. /// 创建点
  3. /// </summary>
  4. private void CreatePoint()
  5. {
  6. canvasRoot.Children.Clear();
  7. int row = 3, column = 3; //三行三列,九宫格
  8. double oneColumnWidth = (this.ActualWidth == 0 ? this.Width : this.ActualWidth) / 3; //单列的宽度
  9. double oneRowHeight = (this.ActualHeight == 0 ? this.Height : this.ActualHeight) / 3; //单列的高度
  10. double leftDistance = (oneColumnWidth - PointSize) / 2; //单列左边距
  11. double topDistance = (oneRowHeight - PointSize) / 2; //单列上边距
  12. for (var i = 0; i < row; i++)
  13. {
  14. for (var j = 0; j < column; j++)
  15. {
  16. Ellipse ellipse = new Ellipse()
  17. {
  18. Width = PointSize,
  19. Height = PointSize,
  20. Fill = Color,
  21. Tag = string.Format("{0}小贝", i, j)
  22. };
  23. Canvas.SetLeft(ellipse, j * oneColumnWidth + leftDistance);
  24. Canvas.SetTop(ellipse, i * oneRowHeight + topDistance);
  25. canvasRoot.Children.Add(ellipse);
  26. ellipseList.Add(ellipse);
  27. }
  28. }
  29. }
复制代码

创建线

  1. private Line CreateLine()
  2. {
  3. Line line = new Line()
  4. {
  5. Stroke = Color,
  6. StrokeThickness = 2
  7. };
  8. return line;
  9. }
复制代码

点和线都创建都定义好了,可以开始监听绘制事件了

  1. private void ScreenUnlock_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
  2. {
  3. if (isChecking) //如果图形正在检查中,不响应后续处理
  4. return;
  5. if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
  6. {
  7. var point = e.GetPosition(this);
  8. HitTestResult result = VisualTreeHelper.HitTest(this, point);
  9. Ellipse ellipse = result.VisualHit as Ellipse;
  10. if (ellipse != null)
  11. {
  12. if (currentLine == null)
  13. {
  14. //从头开始绘制
  15. currentLine = CreateLine();
  16. var ellipseCenterPoint = GetCenterPoint(ellipse);
  17. currentLine.X1 = currentLine.X2 = ellipseCenterPoint.X;
  18. currentLine.Y1 = currentLine.Y2 = ellipseCenterPoint.Y;
  19. currentPointArray.Add(ellipse.Tag.ToString());
  20. Console.WriteLine(string.Join(",", currentPointArray));
  21. currentLineList.Add(currentLine);
  22. canvasRoot.Children.Add(currentLine);
  23. }
  24. else
  25. {
  26. //遇到下一个点,排除已经经过的点
  27. if (currentPointArray.Contains(ellipse.Tag.ToString()))
  28. return;
  29. OnAfterByPoint(ellipse);
  30. }
  31. }
  32. else if (currentLine != null)
  33. {
  34. //绘制过程中
  35. currentLine.X2 = point.X;
  36. currentLine.Y2 = point.Y;
  37. //判断当前Line是否经过点
  38. ellipse = IsOnLine();
  39. if (ellipse != null)
  40. OnAfterByPoint(ellipse);
  41. }
  42. }
  43. else
  44. {
  45. if (currentPointArray.Count == 0)
  46. return;
  47. isChecking = true;
  48. if (currentLineList.Count + 1 != currentPointArray.Count)
  49. {
  50. //最后一条线的终点不在点上
  51. //两点一线,点的个数-1等于线的条数
  52. currentLineList.Remove(currentLine); //从已记录的线集合中删除最后一条多余的线
  53. canvasRoot.Children.Remove(currentLine); //从界面上删除最后一条多余的线
  54. currentLine = null;
  55. }
  56. if (Operation == ScreenUnLockOperationType.Check)
  57. {
  58. Console.WriteLine("playAnimation Check");
  59. var result = CheckPoint(); //执行图形检查
  60.               //执行完成动画并触发检查事件
  61. PlayAnimation(result, () =>
  62. {
  63. if (OnCheckedPoint != null)
  64. {
  65. this.Dispatcher.BeginInvoke(OnCheckedPoint, this, new CheckPointArgs() { Result = result }); //触发检查完成事件
  66. }
  67. });
  68. }
  69. else if (Operation == ScreenUnLockOperationType.Remember)
  70. {
  71. Console.WriteLine("playAnimation Remember");
  72. RememberPoint(); //记忆绘制的坐标
  73. var args = new RememberPointArgs() { PointArray = this.PointArray };
  74.              //执行完成动画并触发记忆事件
  75. PlayAnimation(true, () =>
  76. {
  77. if (OnRememberPoint != null)
  78. {
  79. this.Dispatcher.BeginInvoke(OnRememberPoint, this, args); //触发图形记忆事件
  80. }
  81. });
  82. }
  83. }
  84. }
复制代码

判断线是否经过了附近的某个点

  1. /// <summary>
  2. /// 两点计算一线的长度
  3. /// </summary>
  4. /// <param name="pt1"></param>
  5. /// <param name="pt2"></param>
  6. /// <returns></returns>
  7. private double GetLineLength(double x1, double y1, double x2, double y2)
  8. {
  9. return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); //根据两点计算线段长度公式 √((x1-x2)2x(y1-y2)2)
  10. }
  11. /// <summary>
  12. /// 判断线是否经过了某个点
  13. /// </summary>
  14. /// <param name="ellipse"></param>
  15. /// <returns></returns>
  16. private Ellipse IsOnLine()
  17. {
  18. double lineAB = 0; //当前画线的长度
  19. double lineCA = 0; //当前点和A点的距离
  20. double lineCB = 0; //当前点和B点的距离
  21. double dis = 0;
  22. double deciation = 1; //允许的偏差距离
  23. lineAB = GetLineLength(currentLine.X1, currentLine.Y1, currentLine.X2, currentLine.Y2); //计算当前画线的长度
  24. foreach (Ellipse ellipse in ellipseList)
  25. {
  26. if (currentPointArray.Contains(ellipse.Tag.ToString())) //排除已经经过的点
  27. continue;
  28. var ellipseCenterPoint = GetCenterPoint(ellipse); //取当前点的中心点
  29. lineCA = GetLineLength(currentLine.X1, currentLine.Y1, ellipseCenterPoint.X, ellipseCenterPoint.Y); //计算当前点到线A端的长度
  30. lineCB = GetLineLength(currentLine.X2, currentLine.Y2, ellipseCenterPoint.X, ellipseCenterPoint.Y); //计算当前点到线B端的长度
  31. dis = Math.Abs(lineAB - (lineCA + lineCB)); //线CA的长度+线CB的长度>当前线AB的长度 说明点不在线上
  32. if (dis <= deciation) //因为绘制的点具有一个宽度和高度,所以需设定一个允许的偏差范围,让线靠近点就命中之(吸附效果)
  33. {
  34. return ellipse;
  35. }
  36. }
  37. return null;
  38. }
复制代码

检查点是否正确,按数组顺序逐个匹配之

  1. /// <summary>
  2. /// 检查坐标点是否正确
  3. /// </summary>
  4. /// <returns></returns>
  5. private bool CheckPoint()
  6. {
  7.          //PointArray:正确的坐标值数组
  8.         //currentPointArray:当前绘制的坐标值数组
  9. if (currentPointArray.Count != PointArray.Count)
  10. return false;
  11. for (var i = 0; i < currentPointArray.Count; i++)
  12. {
  13. if (currentPointArray[i] != PointArray[i])
  14. return false;
  15. }
  16. return true;
  17. }
复制代码

记录经过点,并创建一条新的线

  1. /// <summary>
  2. /// 记录经过的点
  3. /// </summary>
  4. /// <param name="ellipse"></param>
  5. private void OnAfterByPoint(Ellipse ellipse)
  6. {
  7. var ellipseCenterPoint = GetCenterPoint(ellipse);
  8. currentLine.X2 = ellipseCenterPoint.X;
  9. currentLine.Y2 = ellipseCenterPoint.Y;
  10. currentLine = CreateLine();
  11. currentLine.X1 = currentLine.X2 = ellipseCenterPoint.X;
  12. currentLine.Y1 = currentLine.Y2 = ellipseCenterPoint.Y;
  13. currentPointArray.Add(ellipse.Tag.ToString());
  14. Console.WriteLine(string.Join(",", currentPointArray));
  15. currentLineList.Add(currentLine);
  16. canvasRoot.Children.Add(currentLine);
  17. }
复制代码
  1. /// <summary>
  2. /// 获取原点的中心点坐标
  3. /// </summary>
  4. /// <param name="ellipse"></param>
  5. /// <returns></returns>
  6. private Point GetCenterPoint(Ellipse ellipse)
  7. {
  8. Point p = new Point(Canvas.GetLeft(ellipse) + ellipse.Width / 2, Canvas.GetTop(ellipse) + ellipse.Height / 2);
  9. return p;
  10. }
复制代码

当绘制完成时,执行完成动画并触发响应模式的事件

  1. /// <summary>
  2. /// 执行动画
  3. /// </summary>
  4. /// <param name="result"></param>
  5. private void PlayAnimation(bool result, Action callback = null)
  6. {
  7. Task.Factory.StartNew(() =>
  8. {
  9. this.Dispatcher.Invoke((Action)delegate
  10. {
  11. foreach (Line l in currentLineList)
  12. l.Stroke = result ? rightColor : errorColor;
  13. foreach (Ellipse e in ellipseList)
  14. if (currentPointArray.Contains(e.Tag.ToString()))
  15. e.Fill = result ? rightColor : errorColor;
  16. });
  17. Thread.Sleep(1500);
  18. this.Dispatcher.Invoke((Action)delegate
  19. {
  20. foreach (Line l in currentLineList)
  21. this.canvasRoot.Children.Remove(l);
  22. foreach (Ellipse e in ellipseList)
  23. e.Fill = Color;
  24. });
  25. currentLine = null;
  26. this.currentPointArray.Clear();
  27. this.currentLineList.Clear();
  28. isChecking = false;
  29. }).ContinueWith(t =>
  30. {
  31. try
  32. {
  33. if (callback != null)
  34. callback();
  35. }
  36. catch (Exception ex)
  37. {
  38. Console.WriteLine(ex.Message);
  39. }
  40. finally
  41. {
  42. t.Dispose();
  43. }
  44. });
  45. }
复制代码

图形解锁的调用

  1. <local:ScreenUnlock Width="500" Height="500"
  2. PointArray="{Binding PointArray, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
  3. Operation="Check"> <!--或Remember-->
  4. <i:Interaction.Triggers>
  5. <i:EventTrigger EventName="OnCheckedPoint">
  6. <Custom:EventToCommand Command="{Binding OnCheckedPoint}" PassEventArgsToCommand="True"/>
  7. </i:EventTrigger>
  8. <i:EventTrigger EventName="OnRememberPoint">
  9. <Custom:EventToCommand Command="{Binding OnRememberPoint}" PassEventArgsToCommand="True"/>
  10. </i:EventTrigger>
  11. </i:Interaction.Triggers>
  12. </local:ScreenUnlock>
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持程序员之家。



回复

使用道具 举报