乐于分享
好东西不私藏

WPF 高颜值手势密码解锁界面(附源码)

WPF 高颜值手势密码解锁界面(附源码)

手势解锁

  • 框架支持.NET4 至 .NET8
  • Visual Studio 2022;

欢迎各位开发者下载并体验。如果在使用过程中遇到任何问题,欢迎随时向我们反馈[3]

项目效果

控件功能
  • 支持鼠标拖动九宫格生成手势密码;
  • State 支持手势成功、失败状态;
  • 可自定义样式、圆角;
  • 事件 GestureCompleted  命令 GestureCompletedCommand 双通道通知解锁结果;
1. 新增 GestureUnlock.cs
  • Password:记录绘制出的密码
  • State:用于控制 GestureItem 颜色(成功/错误);
  • GestureCompletedCommand:支持绑定;
  • GestureCompletedEvent:支持事件;
  • 绘制一个 3x3 网格,每个格子是一个 GestureItem
  • 手势完成后,会触发状态切换,并自动 1.5 秒后清除所有 Ellipse 和 Polyline ;
[TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]publicclassGestureUnlock : Control{privateconststring CanvasTemplateName = "PART_GestureUnlockCanvas";publicstaticreadonly DependencyProperty PasswordProperty =        DependencyProperty.Register("Password"typeof(string), typeof(GestureUnlock),new PropertyMetadata(string.Empty));publicstaticreadonly DependencyProperty StateProperty =        DependencyProperty.Register("State"typeof(GestureState), typeof(GestureUnlock),new PropertyMetadata(GestureState.None, OnIsErrorChanged));publicstaticreadonly DependencyProperty GestureCompletedCommandProperty =        DependencyProperty.Register("GestureCompletedCommand"typeof(ICommand), typeof(GestureUnlock),new PropertyMetadata(null));publicstaticreadonly RoutedEvent GestureCompletedEvent =        EventManager.RegisterRoutedEvent("GestureCompleted", RoutingStrategy.Bubble, typeof(RoutedEventHandler),typeof(GestureUnlock));private Canvas _canvas;privatereadonly List<GestureItem> _gestureItems = new List<GestureItem>();privatereadonly List<int> _gestures = new List<int>();private Polyline _line;private Line _moveLine;privatedouble _thickness = 1;private DispatcherTimer _timer;privatebool _tracking;privatebool _isEventHandled = false;staticGestureUnlock()    {        DefaultStyleKeyProperty.OverrideMetadata(typeof(GestureUnlock),new FrameworkPropertyMetadata(typeof(GestureUnlock)));    }publicGestureUnlock()    {        SizeChanged -= GestureUnlock_SizeChanged;        SizeChanged += GestureUnlock_SizeChanged;    }publicstring Password    {get => (string) GetValue(PasswordProperty);set => SetValue(PasswordProperty, value);    }public GestureState State    {get => (GestureState) GetValue(StateProperty);set => SetValue(StateProperty, value);    }public ICommand GestureCompletedCommand    {get => (ICommand) GetValue(GestureCompletedCommandProperty);set => SetValue(GestureCompletedCommandProperty, value);    }privatestaticvoidOnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)    {var ctrl = d as GestureUnlock;if (ctrl != null)        {            ctrl.UpdateLineStroke();switch (ctrl.State)            {case GestureState.Success:case GestureState.Error:if (ctrl._timer == null)                    {                        ctrl._timer = new DispatcherTimer                        {                            Interval = TimeSpan.FromSeconds(1.5)                        };                        ctrl._timer.Tick += (sender, args) =>                        {                            ctrl._timer.Stop();                            ctrl.CleraGestureAndLine();                        };                    }                    ctrl._timer.Start();break;            }        }    }publicevent RoutedEventHandler GestureCompleted    {add => AddHandler(GestureCompletedEvent, value);remove => RemoveHandler(GestureCompletedEvent, value);    }publicoverridevoidOnApplyTemplate()    {base.OnApplyTemplate();        _canvas = GetTemplateChild(CanvasTemplateName) as Canvas;if (_canvas != null)        {            _canvas.MouseDown -= OnCanvas_MouseDown;            _canvas.MouseDown += OnCanvas_MouseDown;            _canvas.MouseMove -= OnCanvas_MouseMove;            _canvas.MouseMove += OnCanvas_MouseMove;            _canvas.MouseUp -= OnCanvas_MouseUp;            _canvas.MouseUp += OnCanvas_MouseUp;            Loaded += (s, e) => CreateGestureItems();        }    }protectedoverridevoidOnMouseLeave(MouseEventArgs e)    {base.OnMouseLeave(e);if (_tracking && !_isEventHandled)        {            _isEventHandled = true            CompleteGesture();        }        _tracking = false;        _isEventHandled = false;    }privatevoidCleraGestureAndLine()    {        State = GestureState.None;        _gestureItems.ForEach(x => { x.IsSelected = false; });        _gestures.Clear();        _line.Points.Clear();        _isEventHandled = false;    }privatevoidResetMoveLine()    {if (_moveLine != null)        {            _canvas.Children.Remove(_moveLine);            _moveLine = null;        }        _moveLine = new Line        {            Stroke = _gestureItems.Count > 0 ? _gestureItems[0].BorderBrush : ThemeManager.Instance.PrimaryBrush,            StrokeThickness = _thickness > 1 ? _thickness - 1 : _thickness        };    }privatevoidGestureUnlock_SizeChanged(object sender, SizeChangedEventArgs e)    {        CreateGestureItems();    }privatevoidCreateGestureItems()    {        _gestureItems.Clear();        _canvas.Children.Clear();double radius = 20d;int cols = 3int rows = 3double gap = (300 - (3 * radius * 2)) / 4        gap = Math.Max(gap, radius);double totalWidth = (3 * radius * 2) + (2 * gap);double totalHeight = (3 * radius * 2) + (2 * gap);double startX = (ActualWidth - totalWidth) / 2 + radius;double startY = (ActualHeight - totalHeight) / 2 + radius;for (int i = 0; i < 9; i++)        {int col = i % cols;int row = i / rows;double x = startX + col * (gap + radius * 2);double y = startY + row * (gap + radius * 2);var gestureItem = new GestureItem();            gestureItem.Number = i;            gestureItem.Loaded += (s, e) =>            {double actualRadius = Math.Max(gestureItem.ActualWidth, gestureItem.ActualHeight) / 2;                Canvas.SetLeft(gestureItem, x - actualRadius);                Canvas.SetTop(gestureItem, y - actualRadius);                ElementHelper.SetCornerRadius(gestureItem, new CornerRadius(actualRadius));            };            _canvas.Children.Add(gestureItem);            _gestureItems.Add(gestureItem);        }        _line = new Polyline        {            Stroke = _gestureItems.Count > 0 ? _gestureItems[0].BorderBrush : ThemeManager.Instance.PrimaryBrush,            StrokeThickness = _thickness        };        _canvas.Children.Add(_line);        Panel.SetZIndex(_line, -98);        ResetMoveLine();    }privatevoidOnCanvas_MouseUp(object sender, MouseButtonEventArgs e)    {if (!_isEventHandled)        {            _tracking = false;            _isEventHandled = true            CompleteGesture();        }    }voidCompleteGesture()    {if (_gestures.Count == 0return;        Password = string.Join("", _gestures);        GestureCompletedCommand?.Execute(Password);        RaiseEvent(new RoutedEventArgs(GestureCompletedEvent, Password));        ResetMoveLine();    }privatevoidOnCanvas_MouseMove(object sender, MouseEventArgs e)    {if (_tracking && e.LeftButton == MouseButtonState.Pressed)        {var point = e.GetPosition(_canvas);if (_moveLine.X2 == 0 && _moveLine.Y1 == 0)            {                _moveLine.X1 = point.X;                _moveLine.Y1 = point.Y;            }if (_moveLine.X2 != point.X)                _moveLine.X2 = point.X;if (_moveLine.Y2 != point.Y)                _moveLine.Y2 = point.Y;            TryAddPoint(point);        }    }privatevoidOnCanvas_MouseDown(object sender, MouseButtonEventArgs e)    {        CleraGestureAndLine();if (_timer != null)            _timer.Stop();        ResetMoveLine();        _tracking = true;        _isEventHandled = false;var point = e.GetPosition(_canvas);        TryAddPoint(point);    }privatevoidTryAddPoint(Point position)    {for (var i = 0; i < _gestureItems.Count; i++)        {var gestureItem = _gestureItems[i];var left = Canvas.GetLeft(gestureItem);var top = Canvas.GetTop(gestureItem);var centerX = left + gestureItem.Width / 2;var centerY = top + gestureItem.Height / 2;var dist = (position - new Point(centerX, centerY)).Length;if (dist <= gestureItem.Width / 2 && !_gestures.Contains(i))            {if(_gestures.Count == 1)                {var brush = _gestureItems.Count > 0 ? _gestureItems[0].BorderBrush : ThemeManager.Instance.PrimaryBrush;                    _line.Stroke = brush;                    _moveLine.Stroke = brush;                }                _gestures.Add(i);                _line.Points.Add(new Point(centerX, centerY));                gestureItem.IsSelected = true;if (_moveLine.X1 == 0                    &&                     _moveLine.Y1 == 0                    &&                    _moveLine.X2 == 0                    &&                     _moveLine.Y2 == 0)                {                    _moveLine.X1 = centerX;                    _moveLine.Y1 = centerY;                    _canvas.Children.Add(_moveLine);                }else                {var endPoint = _line.Points.LastOrDefault();                    _moveLine.X1 = endPoint.X;                    _moveLine.Y1 = endPoint.Y;                }                Panel.SetZIndex(_moveLine, -99);                _moveLine.X2 = centerX;                _moveLine.Y2 = centerY;break;            }        }    }privatevoidUpdateLineStroke()    {if (_line == null)return;switch (State)        {case GestureState.Success:                _line.Stroke = ThemeManager.Instance.Resources.TryFindResource<Brush>("WD.SuccessBrush");break;case GestureState.Error:                _line.Stroke = ThemeManager.Instance.Resources.TryFindResource<Brush>("WD.DangerBrush");break;default:                _line.Stroke = _gestureItems.Count > 0 ? _gestureItems[0].BorderBrush : ThemeManager.Instance.PrimaryBrush;break;        }    }}
2. 新增 GestureItem.cs
  • Number :用于记录序号;
  • IsSelected:用于标记是否选中;
publicclassGestureItem : Control{publicstaticreadonly DependencyProperty NumberProperty =        DependencyProperty.Register("Number"typeof(int), typeof(GestureItem), new PropertyMetadata(0));publicstaticreadonly DependencyProperty IsSelectedProperty =        DependencyProperty.Register("IsSelected"typeof(bool), typeof(GestureItem), new PropertyMetadata(false));staticGestureItem()    {        DefaultStyleKeyProperty.OverrideMetadata(typeof(GestureItem),new FrameworkPropertyMetadata(typeof(GestureItem)));    }publicint Number    {get => (int) GetValue(NumberProperty);set => SetValue(NumberProperty, value);    }publicbool IsSelected    {get => (bool) GetValue(IsSelectedProperty);set => SetValue(IsSelectedProperty, value);    }}
3. 新增 GestureUnlock.xaml
  • MultiDataTrigger 多数据触发器:
    • Error:设置 BorderBrush 为 DangerBrush 红色;
    • Success:设置 BorderBrush 为 SuccessBrush 绿色;
    • IsSelected:当控件被选中;
    • State:控制颜色(成功/错误)
<Stylex:Key="WD.GestureItem"BasedOn="{StaticResource WD.ControlBasicStyle}"TargetType="{x:Type controls:GestureItem}"><SetterProperty="Width"Value="60" /><SetterProperty="VerticalAlignment"Value="Center" /><SetterProperty="HorizontalAlignment"Value="Center" /><SetterProperty="VerticalContentAlignment"Value="Stretch" /><SetterProperty="HorizontalContentAlignment"Value="Stretch" /><SetterProperty="Height"Value="60" /><SetterProperty="BorderThickness"Value="1.5" /><SetterProperty="Background"Value="#80FFFFFF" /><SetterProperty="BorderBrush"Value="{DynamicResource WD.PrimaryBrush}" /><SetterProperty="Padding"Value="15" /><SetterProperty="Template"><Setter.Value><ControlTemplateTargetType="{x:Type controls:GestureItem}"><Borderx:Name="PART_Border"Width="{TemplateBinding Width}"Height="{TemplateBinding Height}"Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"CornerRadius="{Binding Path=(helpers:ElementHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"UseLayoutRounding="{TemplateBinding UseLayoutRounding}"><Ellipsex:Name="PART_Ellipse"Margin="{TemplateBinding Padding}"HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"VerticalAlignment="{TemplateBinding VerticalContentAlignment}"Fill="{TemplateBinding BorderBrush}"Visibility="Collapsed" /></Border><ControlTemplate.Triggers><TriggerProperty="IsSelected"Value="True"><SetterTargetName="PART_Border"Property="Background"Value="{DynamicResource WD.BackgroundBrush}" /><SetterTargetName="PART_Ellipse"Property="Visibility"Value="Visible" /></Trigger><MultiDataTrigger><MultiDataTrigger.Conditions><ConditionBinding="{Binding IsSelected, RelativeSource={RelativeSource Self}}"Value="True" /><ConditionBinding="{Binding State, RelativeSource={RelativeSource AncestorType=controls:GestureUnlock}}"Value="Error" /></MultiDataTrigger.Conditions><SetterProperty="BorderBrush"Value="{DynamicResource WD.DangerBrush}" /></MultiDataTrigger><MultiDataTrigger><MultiDataTrigger.Conditions><ConditionBinding="{Binding IsSelected, RelativeSource={RelativeSource Self}}"Value="True" /><ConditionBinding="{Binding State, RelativeSource={RelativeSource AncestorType=controls:GestureUnlock}}"Value="Success" /></MultiDataTrigger.Conditions><SetterProperty="BorderBrush"Value="{DynamicResource WD.SuccessBrush}" /></MultiDataTrigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter></Style><Stylex:Key="WD.GestureUnlock"BasedOn="{StaticResource WD.ControlBasicStyle}"TargetType="{x:Type controls:GestureUnlock}"><SetterProperty="Background"Value="{DynamicResource WD.BackgroundBrush}" /><SetterProperty="Width"Value="300" /><SetterProperty="VerticalAlignment"Value="Center" /><SetterProperty="HorizontalAlignment"Value="Center" /><SetterProperty="Height"Value="300" /><SetterProperty="Template"><Setter.Value><ControlTemplateTargetType="{x:Type controls:GestureUnlock}"><controls:WDBorderCornerRadius="{Binding Path=(helpers:ElementHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"><Canvasx:Name="PART_GestureUnlockCanvas"Background="{TemplateBinding Background}"Clip="{Binding RelativeSource={RelativeSource AncestorType=controls:WDBorder}, Path=ContentClip}"SnapsToDevicePixels="True" /></controls:WDBorder></ControlTemplate></Setter.Value></Setter></Style><StyleBasedOn="{StaticResource WD.GestureItem}"TargetType="{x:Type controls:GestureItem}" /><StyleBasedOn="{StaticResource WD.GestureUnlock}"TargetType="{x:Type controls:GestureUnlock}" />
3. 新增 GestureUnlockExample.xaml
  • TabItem Header="Event" 事件示例;
  • TabItem Header="Command" 命令示例;
<TabControlTabStripPlacement="Bottom"><TabItemHeader="Event"><wd:GestureUnlockx:Name="myGestureUnlock"wd:ElementHelper.CornerRadius="5"GestureCompleted="GestureCompleted"><wd:GestureUnlock.Background><ImageBrushImageSource="pack://application:,,,/WPFDevelopers.Samples;component/Resources/Images/Chat/Right.jpg" /></wd:GestureUnlock.Background></wd:GestureUnlock></TabItem><TabItemHeader="Command"><TabItem.Resources><StyleBasedOn="{StaticResource WD.GestureItem}"TargetType="wd:GestureItem"><SetterProperty="BorderBrush"Value="Yellow" /></Style></TabItem.Resources><wd:GestureUnlockx:Name="myGestureUnlock2"Width="400"Height="500"wd:ElementHelper.CornerRadius="10"GestureCompletedCommand="{Binding GestureCompletedCommand}"><wd:GestureUnlock.Background><ImageBrushImageSource="pack://application:,,,/WPFDevelopers.Samples;component/Resources/Images/IconicThumbnail/LcupqA.jpg" /></wd:GestureUnlock.Background></wd:GestureUnlock></TabItem></TabControl>
4. 新增 GestureUnlockExample.xaml.cs
  • _password 字段存正确的密码 "0426"
  • HandleGestureUnlock 手势解锁方法:
    • 如密码长度小于 4 位,将状态设置 Error 并 Push 错误消息;
    • 如密码与 _password 不匹配,也将状态设置 Error 并 Push 错误消息;
    • 如密码正确,状态设置 Success,并提示解锁成功
publicpartialclassGestureUnlockExample : UserControl{privatestring _password = "0426";privateenum GestureUnlockType    {        Unlock1,        Unlock2    }publicGestureUnlockExample()    {        InitializeComponent();        DataContext = this;    }privatevoidHandleGestureUnlock(string pwd, GestureUnlockType unlockType)    {if (pwd.Length < 4)        {            SetGestureState(unlockType, GestureState.Error);            Message.PushDesktop("手势错误,最少 4 个节点!", MessageBoxImage.Error, true);return;        }if (pwd != _password)        {            SetGestureState(unlockType, GestureState.Error);            Message.PushDesktop("手势错误,请重新解锁!", MessageBoxImage.Error, true);return;        }        SetGestureState(unlockType, GestureState.Success);        Message.Push("手势正确!", MessageBoxImage.Information, true);    }privatevoidSetGestureState(GestureUnlockType unlockType, GestureState state)    {if (unlockType == GestureUnlockType.Unlock1)        {            myGestureUnlock.State = state;        }elseif (unlockType == GestureUnlockType.Unlock2)        {            myGestureUnlock2.State = state;        }    }privatevoidGestureCompleted(object sender, RoutedEventArgs e)    {var pwd = e.OriginalSource.ToString();        HandleGestureUnlock(pwd, GestureUnlockType.Unlock1);    }public ICommand GestureCompletedCommand => new RelayCommand(param =>    {var pwd = param.ToString();        HandleGestureUnlock(pwd, GestureUnlockType.Unlock2);    });}
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

作者:小码编匠

出处:gitee.com/smallcore/DotNetCore
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!
END
方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群

推荐阅读

Avalonia 工业级实战:从 PLC 通信到 Web HMI 的完整方案

一个值得收藏的 WinForms 界面框架:流式菜单 + 分割容器 + 多标签页

C# 做动态数据看板?这个 WinForms 多图表方案值得参考

C# 面向自动化产线上位机开源项目(支持报警、日志与多语言)

真正能落地的 .NET 8/9/10 企业平台:集成权限、流程引擎与实时通信

WPF 工业组态界面既专业又现代?HandyControl + ElementUI 风格

.NET 8.0 开发的工业控制领域气密性检测系统

不玩虚的,这款开源 .NET 低代码平台,开箱即用流程引擎、BI 报表、权限控制

WPF 双模式工业温湿度监控上位机,支持独立运行与 MES 对接

WinForm 过时了?3月技术盘点:类IDE上位机、3D仿真与 Modbus调试工具

不靠框架,一套能用的 WinForm 企业人事管理系统(附源码)

C# 工业级温度监控软件:支持多PLC通信与实时曲线绘制

基于 WinForms 实现多设备、多语言的 HMI 上位机框架

WPF 桌面也能做工业级看板?LiveCharts 让数据可视化更出彩

C# 统一工业 CAN 设备通信:跨平台、多厂商、高性能的通信库

一文看懂 CAN 通信:C# 实现上位机通信方法

从零搭建视觉系统?这个 .NET 工业视觉平台值得参考

WinForm + PLC + SQLite 的上位机项目,真的值得你收藏!

.NET 8 + WPF 做工业机器人3D仿真?HelixToolkit 真香

.NET 8 打造工业级运动控制系统

C# 实现工控机硬件监控与看门狗系统,别再让工控机死机

AI 辅助开发如何重塑 .NET 9 + WPF 企业级应用架构?

C# 开发 Modbus 通信?这两大开源库你必须了解

WinForm 自适应布局神器:告别手动计算,轻松实现专业界面

用 C# + WinForms 手撸一个轻量级矢量图绘制系统

WPF + MVVM 工业生产监控平台,用户控件动态加载与流畅动画实现

VisionMaster 通讯太麻烦?.NET 8 + TCP 为工业视觉定制的轻量通信方案

WinForm 打造类 IDE 的运动控制上位机

谁说 .NET 桌面过时了?AI 协作开发 WPF + SQLite 监控工具

WinForm 实现的工业视觉流程编排系统,告别硬编码
基于 .NET 的 YOLO 多模型推理平台开源了
C# 工业开发必备:20+ 开源组件大合集(S7 模拟/流程图引擎/YOLO 视觉)
C# + Halcon 打造 VisionPro 风格的拖拽式视觉工具
WinForms 工业 HMI 上位机框架,Modbus TCP + MQTT 都搞定了
C# 打造工业级 SCADA 系统,从零搭建智慧加压站监控平台
WPF + Modbus 打造轻量级工业数据采集与监控系统
.NET 8 + WPF 打造多协议 PLC 通讯平台,工业数据采集从未如此简单
从零实现 WinForm 运动控制上位机:点动、急停、自动运行全搞定
.NET 好用的 PLC 通信网关,支持多品牌工业设备
不用真实 PLC?这个 C# 模拟器让上位机开发随时联调 S7
.NET 8 + WPF 做工业软件?这个 MES 项目值得参考
C# 零依赖 YOLO 图像标注器 OpenCvSharp 与 GDI+ 双实现
C# 轻量级工业温湿度监控系统(含数据库与源码)
C# 工业级流程图控件:轻量、高效、可交互
C# 工控精选 20+开源项目(含PLC模拟、状态机、高颜值HMI)
C# + Halcon 打造你的可视化机器视觉流程编辑器
C# 工厂自动化实战:用软PLC + HMI 一体化开发控制系统
别再说 C# 做不了工业视觉!多相机 + 插件架构 + 全流程管控全落地
C# 打造自己的 PLC 模拟器:无需硬件也能开发上位机
.NET 9 + Avalonia 实现跨平台 AI 标注工具,一键自动标注 YOLO 目标
C# 实现 Visual Studio 风格的 WinForms 可视化设计器

觉得有收获?不妨分享让更多人受益

关注「DotNet技术匠」,共同提升技术实力

收藏
点赞
分享
在看