当前位置: 首页 > news >正文

MouseOverShapeBox

MouseOverShapeBox

MouseOverShapeBox 源码详解 - 鼠标悬停高亮标注框

这是一个增强版图像标注控件,当鼠标移动到形状上时,会高亮显示该形状。类似于图片标注工具中鼠标悬停时边框变色的效果。


📄 文件头部(版权信息)

// 同之前的ShapeBox,版权归HeBianGu所有,MIT开源协议// Copyright (c) HeBianGu Authors. All Rights Reserved.// ...(省略相同部分)

📦 引用命名空间

usingH.Extensions.Common;// 扩展方法库(如ToEnumerable)usingSystem.Collections.Specialized;// 集合变化通知(NotifyCollectionChangedEventArgs)usingSystem.Windows.Input;// 鼠标输入相关(MouseEventArgs、ICommand)

🏗️ 类定义

// 继承自ShapeBox,增加鼠标悬停高亮功能publicclassMouseOverShapeBox:ShapeBox

🎨 私有字段

// 专门用来绘制"鼠标悬停高亮"形状的视觉对象// 这个图层独立于普通形状图层,确保高亮效果不受其他绘制影响privateDrawingVisual_MouseOverableShapeDrawingVisual=newDrawingVisual();

🖼️ 重写创建视觉对象方法

protectedoverrideIEnumerable<Visual>CreateVisuals(){// base.CreateVisuals() = 调用父类方法,返回[图片层, 普通形状层]// .Concat() = 连接两个集合// this._MouseOverableShapeDrawingVisual.ToEnumerable() = 把高亮图层转成集合// 最终返回:[图片层, 普通形状层, 高亮图层]// 高亮图层在最上层,确保高亮效果可见returnbase.CreateVisuals().Concat(this._MouseOverableShapeDrawingVisual.ToEnumerable());}

通俗理解:就像三张透明玻璃叠在一起:

  1. 底层:背景图片
  2. 中层:普通形状(矩形、圆形等)
  3. 顶层:鼠标悬停高亮效果

🎨 鼠标悬停边框颜色属性

// 鼠标悬停时的边框颜色(默认黄绿色)publicBrushMouseOverStroke{get{return(Brush)GetValue(MouseOverStrokeProperty);}set{SetValue(MouseOverStrokeProperty,value);}}// 依赖属性注册publicstaticreadonlyDependencyPropertyMouseOverStrokeProperty=DependencyProperty.Register("MouseOverStroke",typeof(Brush),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(Brushes.Chartreuse,// 默认黄绿色(d,e)=>{MouseOverShapeBoxcontrol=dasMouseOverShapeBox;if(control==null)return;// 这里可以添加颜色变化时的处理逻辑}));

📏 鼠标悬停边框粗细属性

// 鼠标悬停时的边框粗细(默认1.0像素)publicdoubleMouseOverStrokeThickness{get{return(double)GetValue(MouseOverStrokeThicknessProperty);}set{SetValue(MouseOverStrokeThicknessProperty,value);}}publicstaticreadonlyDependencyPropertyMouseOverStrokeThicknessProperty=DependencyProperty.Register("MouseOverStrokeThickness",typeof(double),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(1.0,// 默认1像素,比普通边框可能更粗,突出显示(d,e)=>{}));

🎨 鼠标悬停填充颜色属性

// 鼠标悬停时的填充颜色(形状内部的颜色)publicBrushMouseOverFill{get{return(Brush)GetValue(MouseOverFillProperty);}set{SetValue(MouseOverFillProperty,value);}}publicstaticreadonlyDependencyPropertyMouseOverFillProperty=DependencyProperty.Register("MouseOverFill",typeof(Brush),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(default(Brush),// 默认null(透明)(d,e)=>{}));

使用示例

  • 普通状态:红色边框,透明填充
  • 鼠标悬停:黄色边框,半透明蓝色填充

🔍 重写缩放变化处理

protectedoverridevoidOnScaleChanged(){// 先调用父类方法(重绘普通形状)base.OnScaleChanged();// 如果鼠标悬停边框不是自动模式if(this.MouseOverStrokeThickness>0)// 重新绘制高亮形状(适应新的缩放比例)this.DrawMouseOverableShapes();}

为什么需要这个?缩放时,边框粗细也需要按比例调整,否则会显得太粗或太细。


📝 重写形状变化通知

protectedoverridevoidOnShapesChanged(){// 先调用父类方法base.OnShapesChanged();// 重新检测鼠标下的形状(因为形状列表变了)this.MouseOverShapes();}

场景:当添加或删除形状时,鼠标下的形状可能变化,需要重新计算。


🔄 重写完整更新方法

publicoverridevoidUpdateAll(){// 先调用父类方法(更新图片和普通形状)base.UpdateAll();// 再绘制高亮形状(确保高亮效果最新)this.DrawMouseOverableShapes();}

🖱️ 鼠标移动事件(核心逻辑)

protectedoverridevoidOnMouseMove(MouseEventArgse){// 先调用父类方法(确保其他鼠标功能正常)base.OnMouseMove(e);// 如果没有形状集合,直接返回if(this.Shapes==null)return;// 获取鼠标相对于当前控件的位置Pointpoint=e.GetPosition(this);// 查找所有鼠标下的形状:// 1. OfType<IMouseOverShape>() - 只关心支持鼠标悬停的形状// 2. Where(x => x.Hit(this, point)) - 检测鼠标是否在形状内varfinds=this.Shapes.OfType<IMouseOverShape>().Where(x=>x.Hit(this,point));// 将找到的形状传递给高亮显示方法this.MouseOverShapes(finds.ToArray());}

Hit方法原理

  • 对于矩形:判断点是否在矩形范围内
  • 对于圆形:判断点到圆心的距离是否小于半径
  • 对于多边形:使用射线法判断

📋 管理高亮形状的列表

// 私有字段:存储当前鼠标下的所有形状privateList<IMouseOverShape>_MouseOverShapes=newList<IMouseOverShape>();// 设置当前高亮的形状protectedvirtualvoidMouseOverShapes(paramsIMouseOverShape[]MouseOverableShapes){// 清空之前的列表this._MouseOverShapes.Clear();// 如果传入了新形状,添加到列表if(MouseOverableShapes!=null)this._MouseOverShapes.AddRange(MouseOverableShapes);// 重新绘制高亮效果this.DrawMouseOverableShapes();}

为什么用列表?因为形状可能重叠,鼠标可能同时悬停在多个形状上。


✏️ 绘制高亮形状(核心方法)

privatevoidDrawMouseOverableShapes(){// 打开高亮图层的绘制上下文usingvardrawingContext=this._MouseOverableShapeDrawingVisual.RenderOpen();// 如果没有需要高亮的形状,直接返回(清除之前的高亮)if(this._MouseOverShapes==null||this._MouseOverShapes.Count()==0)return;// 计算视图中的边框粗细(考虑缩放)varstrokeThickness=this.ToViewThickness(this.MouseOverStrokeThickness);// 遍历每个需要高亮的形状// .Where(x => this.Shapes.Contains(x)) - 确保形状还在集合中(防止已被删除)foreach(variteminthis._MouseOverShapes.Where(x=>this.Shapes.Contains(x))){// 调用形状自己的DrawMouseOver方法// 让形状自己绘制高亮效果(可以自定义样式)item.DrawMouseOver(this,drawingContext,this.MouseOverStroke,// 高亮边框颜色strokeThickness,// 高亮边框粗细this.MouseOverFill);// 高亮填充颜色}}

🔔 重写集合变化通知

protectedoverridevoidShapesCollectionChanged(objectsender,NotifyCollectionChangedEventArgse){// 先调用父类方法(重绘普通形状)base.ShapesCollectionChanged(sender,e);// 重绘高亮形状(因为形状集合变了,高亮状态可能需要更新)this.DrawMouseOverableShapes();}

场景

  • 删除了一个形状 → 如果它正好高亮,需要清除高亮
  • 添加了新形状 → 检查鼠标是否在新形状上

🎯 总体设计思路

架构图

┌─────────────────────────────────────────────┐ │ MouseOverShapeBox 控件 │ ├─────────────────────────────────────────────┤ │ ┌───────────────────────────────────────┐ │ │ │ 高亮图层 (MouseOverableShapeDrawingVisual)│ ← 鼠标悬停效果(最上层) │ ├───────────────────────────────────────┤ │ │ │ 普通形状层 (ShapeDrawingVisual) │ │ ← 普通矩形、圆形等 │ ├───────────────────────────────────────┤ │ │ │ 图片层 (ImageDrawingVisual) │ │ ← 背景图片 │ └───────────────────────────────────────┘ │ └─────────────────────────────────────────────┘

工作流程

用户移动鼠标 ↓ OnMouseMove 触发 ↓ 获取鼠标坐标 ↓ 遍历所有形状,检测哪些被鼠标击中 ↓ 更新 _MouseOverShapes 列表 ↓ DrawMouseOverableShapes 重绘高亮 ↓ 用户看到高亮效果

核心特性对比

特性ShapeBoxMouseOverShapeBox
显示图片
绘制形状
鼠标悬停高亮
高亮样式可配置✅(颜色、粗细、填充)
支持形状重叠✅(可同时高亮多个)

使用示例

// 创建鼠标悬停标注框varbox=newMouseOverShapeBox();box.ImageSource=myImage;// 添加几个矩形box.Shapes.Add(newRectangleShape{Bounds=newRect(10,10,100,100)});box.Shapes.Add(newRectangleShape{Bounds=newRect(50,50,100,100)});// 配置普通样式box.Stroke=Brushes.Blue;// 普通边框蓝色box.StrokeThickness=1;// 普通边框1像素// 配置悬停样式box.MouseOverStroke=Brushes.Red;// 悬停边框红色box.MouseOverStrokeThickness=3;// 悬停边框3像素(更醒目)box.MouseOverFill=Brushes.Yellow;// 悬停填充黄色// 运行后,鼠标移动到矩形上时,会看到红色粗边框 + 黄色填充

设计模式识别

  1. 模板方法模式OnMouseMoveDrawMouseOverableShapes
  2. 装饰器模式:在原有ShapeBox功能上增加鼠标悬停功能
  3. 观察者模式:监听鼠标移动和形状集合变化
  4. 策略模式IMouseOverShape接口,不同形状实现不同的Hit检测

关键接口 - IMouseOverShape

// 形状需要实现这个接口才能支持鼠标悬停publicinterfaceIMouseOverShape:IShape{// 检测鼠标是否在形状内boolHit(MouseOverShapeBoxbox,Pointpoint);// 绘制高亮效果voidDrawMouseOver(MouseOverShapeBoxbox,DrawingContextdc,Brushstroke,doublethickness,Brushfill);}

这个控件让你的标注工具更交互友好,用户能清楚地知道当前鼠标指向哪个标注框!

// Copyright (c) HeBianGu Authors. All Rights Reserved.// Author: HeBianGu// Github: https://github.com/HeBianGu/WPF-Control// Document: https://hebiangu.github.io/WPF-Control-Docs// QQ:908293466 Group:971261058// bilibili: https://space.bilibili.com/370266611// Licensed under the MIT License (the "License")usingH.Extensions.Common;usingSystem.Collections.Specialized;usingSystem.Windows.Input;namespaceH.LabelImg.ShapeBox;publicclassMouseOverShapeBox:ShapeBox{privateDrawingVisual_MouseOverableShapeDrawingVisual=newDrawingVisual();protectedoverrideIEnumerable<Visual>CreateVisuals(){returnbase.CreateVisuals().Concat(this._MouseOverableShapeDrawingVisual.ToEnumerable());}publicBrushMouseOverStroke{get{return(Brush)GetValue(MouseOverStrokeProperty);}set{SetValue(MouseOverStrokeProperty,value);}}publicstaticreadonlyDependencyPropertyMouseOverStrokeProperty=DependencyProperty.Register("MouseOverStroke",typeof(Brush),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(Brushes.Chartreuse,(d,e)=>{MouseOverShapeBoxcontrol=dasMouseOverShapeBox;if(control==null)return;if(e.OldValueisBrusho){}if(e.NewValueisBrushn){}}));publicdoubleMouseOverStrokeThickness{get{return(double)GetValue(MouseOverStrokeThicknessProperty);}set{SetValue(MouseOverStrokeThicknessProperty,value);}}publicstaticreadonlyDependencyPropertyMouseOverStrokeThicknessProperty=DependencyProperty.Register("MouseOverStrokeThickness",typeof(double),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(1.0,(d,e)=>{MouseOverShapeBoxcontrol=dasMouseOverShapeBox;if(control==null)return;if(e.OldValueisdoubleo){}if(e.NewValueisdoublen){}}));publicBrushMouseOverFill{get{return(Brush)GetValue(MouseOverFillProperty);}set{SetValue(MouseOverFillProperty,value);}}// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...publicstaticreadonlyDependencyPropertyMouseOverFillProperty=DependencyProperty.Register("MouseOverFill",typeof(Brush),typeof(MouseOverShapeBox),newFrameworkPropertyMetadata(default(Brush),(d,e)=>{MouseOverShapeBoxcontrol=dasMouseOverShapeBox;if(control==null)return;if(e.OldValueisBrusho){}if(e.NewValueisBrushn){}}));protectedoverridevoidOnScaleChanged(){base.OnScaleChanged();if(this.MouseOverStrokeThickness>0)this.DrawMouseOverableShapes();}protectedoverridevoidOnShapesChanged(){base.OnShapesChanged();this.MouseOverShapes();}publicoverridevoidUpdateAll(){base.UpdateAll();this.DrawMouseOverableShapes();}protectedoverridevoidOnMouseMove(MouseEventArgse){base.OnMouseMove(e);if(this.Shapes==null)return;Pointpoint=e.GetPosition(this);varfinds=this.Shapes.OfType<IMouseOverShape>().Where(x=>x.Hit(this,point));this.MouseOverShapes(finds.ToArray());}privateList<IMouseOverShape>_MouseOverShapes=newList<IMouseOverShape>();protectedvirtualvoidMouseOverShapes(paramsIMouseOverShape[]MouseOverableShapes){this._MouseOverShapes.Clear();if(MouseOverableShapes!=null)this._MouseOverShapes.AddRange(MouseOverableShapes);this.DrawMouseOverableShapes();}privatevoidDrawMouseOverableShapes(){usingvardrawingContext=this._MouseOverableShapeDrawingVisual.RenderOpen();if(this._MouseOverShapes==null||this._MouseOverShapes.Count()==0)return;varstrokeThickness=this.ToViewThickness(this.MouseOverStrokeThickness);foreach(variteminthis._MouseOverShapes.Where(x=>this.Shapes.Contains(x))){item.DrawMouseOver(this,drawingContext,this.MouseOverStroke,strokeThickness,this.MouseOverFill);}}protectedoverridevoidShapesCollectionChanged(objectsender,NotifyCollectionChangedEventArgse){base.ShapesCollectionChanged(sender,e);this.DrawMouseOverableShapes();}}
http://www.jsqmd.com/news/580357/

相关文章:

  • NCM音乐解密转换全攻略:轻松解锁网易云音乐加密格式
  • 基于Matlab实现汽车运动状态估计:卡尔曼+强跟踪+自适应滤波跟踪算法实践
  • Windows 11 Android应用生态完整指南:免费实现跨平台融合
  • Windows驱动存储深度管理:DriverStore Explorer全方位解决方案
  • Phi-4-mini-reasoning实操手册:批量prompt推理与结果结构化存储
  • douyin-downloader:解决音视频资源高效获取痛点的全流程解决方案
  • OpenCore Legacy Patcher终极指南:让老旧Mac突破限制,焕发新生
  • 别再只配AP了!深度解析神州数码AC无线IP地址选举机制:为什么你的AP总注册失败?
  • Claude Code 常用技巧:这几个操作让我开发效率翻倍
  • Pixel Dimension Fissioner 效果深度评测:对比YOLOv8目标检测的图像风格迁移应用
  • 一代神车斯柯达要谢幕了?为啥要退出中国市场?
  • 51万行源码全网疯传:Claude Code泄露事件,为何让全球开发者集体狂欢?又将如何改写AI工具的竞争终局?
  • 5种核心能力解析:抖音无水印视频下载工具DouYinBot全指南
  • FastAPI 实战项目:从 0 到 1 搭一个类似 Netflix Dispatch 的事件管理后端
  • Unity游戏引擎集成豆包Doubao-1.5-pro-32k:实现实时AI对话与流式响应
  • 如何快速配置Zotero插件:终极管理解决方案与插件市场指南
  • Pixel Epic效果展示:跨语言研报生成(中英双语对照版)实测案例
  • 2026 科技大厂裁员真相:AI 不是借口
  • 开源阅读鸿蒙版完整指南:打造你的专属数字图书馆
  • PbootCMS 如何利用 Schema 结构化数据优化 SEO_PbootCMS 如何防止网站内容被重复收录
  • Windows热键冲突终极方案:3分钟定位占用程序的智能侦探
  • Leather Dress Collection 快速原型展示:10类行业应用创意集锦
  • Qwen Pixel Art在教育场景的应用:编程课教学生生成像素动画教学素材
  • OBS Multi RTMP插件:如何一键实现多平台直播推流
  • 智能工具如何提升碧蓝航线游戏效率:从重复操作中解放的实战指南
  • 万象视界灵坛应用场景:跨境电商商品图自动匹配多语言语义标签
  • OCRmyPDF终极指南:5分钟让扫描PDF变可搜索文档
  • Thorium浏览器:超越Chromium的性能怪兽与隐私守护者
  • cv_resnet101_face-detection_cvpr22papermogface企业应用:银行柜台人脸识别预处理工具
  • AudioLDM-S场景解析:如何用AI音效提升短视频、游戏开发的创作效率