AGV地图编辑软件 ------ 一步一步开始开发之主界面设计代码
概述
本次写博客采取总分总的模式,也不知大家适不适应,前篇我们介绍了AGV地图编辑软件的大概功能,接下来我们来介绍如何用代码来实现。写博客主要是为了可以认识更多优秀的志趣相投的人,共同学习、进步。
主界面设计
先来看下主画面的布局,都是基础控件,就不在此进行细述了,见下面图片说明:
首先,我们需要在上图的Panel中添加一个我们自己需要的自定义组件,在项目中新建一个用户自定义控件的文件夹,命名为AGVMapUControl(至于取什么名字你们随意)
接着,我们右键该文件夹,选择添加,然后添加一个组件,命名为:MapPanel,按F7进入代码视图,让该组件继承自控件Panel
public partial class MapPanel : Panel
为了减小组件重绘时的闪烁,我们可以在构造函数中开启双缓存
public MapPanel()
{
InitializeComponent();
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);//先在缓存区绘制,而不是直接绘制到界面上,减少闪烁
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);//设置自行绘制,而不是系统绘制
SetStyle(ControlStyles.DoubleBuffer, true);//双缓存开启
this.UpdateStyles();
}
接着我们重写控件的几个事件,重绘事件:用于控件的刷新;鼠标点击事件:当有站点、停车充电点
、直线路径、贝塞尔曲线路径选中进行标记为其他颜色,属性框内对应的属性跳转为选中的单元的各个属性值;鼠标移动事件:当鼠标按下并且进行移动时,绘制拖拽框,类似于windows自带的绘图软件的拖拽框;鼠标抬起事件:确定拖拽框最终的大小
#region 重绘事件
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
var g = pe.Graphics;
}
#endregion
#region 鼠标点击时,选中其中一条直线或者曲线则将其他的全部取消标记
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
Focus();
Invalidate();
}
#endregion
#region 拖拽框变量
/// <summary>
/// 记录移动位置
/// </summary>
private Point MouseStartPoint;
/// <summary>
/// 拖选坐标
/// </summary>
private Point MouseEndPoint;
/// <summary>
/// 拖放矩形
/// </summary>
private Rectangle DragRectangle;
/// <summary>
/// 判断鼠标是否按下
/// </summary>
bool isMouseDown = false;
#endregion
#region 鼠标按下事件,用于绘制拖动框
protected override void OnMouseDown(MouseEventArgs e)
{
MouseStartPoint = e.Location; //记录鼠标左键按下时位置
MouseEndPoint = e.Location; //初始化结束位置
isMouseDown = true; //鼠标按下了
base.OnMouseDown(e);
}
#endregion
#region 鼠标移动时,画一个矩形框
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
MouseEndPoint = e.Location;//确定拖动框的终点坐标
if (e.Button == MouseButtons.Left)
{
if (isMouseDown)
{
if (!DragRectangle.Size.IsEmpty)
{
#region 计算拖拽边框的大小
var moveX = e.Location.X - MouseStartPoint.X;
var moveY = e.Location.Y - MouseStartPoint.Y;
if (moveX < 0)
{
DragRectangle.X = MouseStartPoint.X + moveX;
DragRectangle.Width = Math.Abs(moveX);
}
else
{
DragRectangle.X = MouseStartPoint.X;
DragRectangle.Width = Math.Abs(moveX);
}
if (moveY < 0)
{
DragRectangle.Y = MouseStartPoint.Y + moveY;
DragRectangle.Height = Math.Abs(moveY);
}
else
{
DragRectangle.Y = MouseStartPoint.Y;
DragRectangle.Height = Math.Abs(moveY);
}
#endregion
#region 画拖拽矩形框
var p = new Pen(Color.DeepPink, 1);
p.DashStyle = DashStyle.Solid;
CreateGraphics().DrawRectangle(p, DragRectangle);
if (point != MouseEndPoint)
{
point = MouseEndPoint;
Invalidate();
}
#endregion
}
}
}
else
{
DragRectangle.Width = 0;
DragRectangle.Height = 0;
}
}
#endregion
#region 鼠标抬起事件,用于确定绘制拖动框的大小,判断框内是否有站点/停车充电点被选中
protected override void OnMouseUp(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isMouseDown = false;//鼠标按下标志位设置为false
if (DragRectangle.IsEmpty)
{
return;
}
}
//---重置绘画的边框高度和宽度
DragRectangle.Width = 0;
DragRectangle.Height = 0;
base.OnMouseUp(e);
}
#endregion
#region 鼠标移出时
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
}
#endregion
至此,已经将控件的事件进行了重写声明,接着,我们需要绑定各种属性,我们可以先添加一个基类,先添加一个文件夹,命名为Property,然后右键该文件夹->添加->类,命名为:PropertyBase,让他继承于 ICustomTypeDescriptor(为对象提供动态自定义类型信息的接口)
然后我们显示实现对应的接口方法,具体代码详见下面
public class PropertyBase : ICustomTypeDescriptor
{
#region ICustomTypeDescriptor 显式接口定义
/// <summary>
/// 为指定组件返回特性集合
/// </summary>
/// <returns>具有组件的特性的 System.ComponentModel.AttributeCollection。 如果组件为 null,则此方法返回一个空集合。</returns>
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(this, true);
}
/// <summary>
/// 使用自定义类型描述符返回指定组件的类的名称。
/// </summary>
/// <returns>一个包含指定组件的类的名称的 System.String。</returns>
string ICustomTypeDescriptor.GetClassName()
{
return TypeDescriptor.GetClassName(this, true);
}
/// <summary>
/// 使用自定义类型描述符返回指定组件的名称。
/// </summary>
/// <returns>为指定组件的类名,或者,如果不存在任何组件名,则为 null。</returns>
string ICustomTypeDescriptor.GetComponentName()
{
return TypeDescriptor.GetComponentName(this, true);
}
/// <summary>
/// 为具有自定义类型描述符的指定组件类型返回一个类型转换器。
/// </summary>
/// <returns>指定组件的 System.ComponentModel.TypeConverter。</returns>
TypeConverter ICustomTypeDescriptor.GetConverter()
{
return TypeDescriptor.GetConverter(this, true);
}
/// <summary>
/// 返回具有自定义类型描述符的组件的默认事件。
/// </summary>
/// <returns>带有默认事件的 System.ComponentModel.EventDescriptor;如果没有事件,则为 null。</returns>
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this, true);
}
/// <summary>
/// 返回指定类型组件的默认属性。
/// </summary>
/// <returns>具有默认属性的 System.ComponentModel.PropertyDescriptor;如果没有属性,则为 null。</returns>
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return null;
}
/// <summary>
/// 为指定组件返回具有指定基类型和自定义类型描述符的编辑器。
/// </summary>
/// <param name="editorBaseType">表示要查找的编辑器的基类型的 System.Type。</param>
/// <returns>可转换为指定编辑器类型的编辑器的一个实例,如果找不到请求类型的编辑器,则为 null。</returns>
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType, true);
}
/// <summary>
/// 对具有自定义类型描述符的指定组件,返回事件集合。
/// </summary>
/// <returns>具有此组件的事件的 System.ComponentModel.EventDescriptorCollection。</returns>
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return TypeDescriptor.GetEvents(this, true);
}
/// <summary>
/// 通过使用指定的属性数组作为筛选器,并使用自定义类型描述符来返回指定组件的事件集合。
/// </summary>
/// <param name="attributes">要用作筛选器的类型 System.Attribute 数组。</param>
/// <returns>具有匹配此组件指定属性的事件的 System.ComponentModel.EventDescriptorCollection。</returns>
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes, true);
}
/// <summary>
/// 使用特性数组作为筛选器,返回此组件实例的属性。
/// </summary>
/// <returns>一个 System.ComponentModel.PropertyDescriptorCollection,它表示此组件实例的已筛选属性。</returns>
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return ((ICustomTypeDescriptor)this).GetProperties(new Attribute[0]);
}
/// <summary>
///
/// </summary>
/// <param name="attributes"></param>
/// <returns></returns>
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
ArrayList props = new ArrayList();
Type thisType = GetType();
PropertyInfo[] pis = thisType.GetProperties();
foreach (PropertyInfo p in pis)
{
if (p.DeclaringType == thisType || p.PropertyType.ToString() == "System.Drawing.Color")
{
//判断属性是否显示
BrowsableAttribute Browsable = (BrowsableAttribute)Attribute.GetCustomAttribute(p, typeof(BrowsableAttribute));
if (Browsable != null)
{
if (Browsable.Browsable || p.PropertyType.ToString() == "System.Drawing.Color")
{
PropertyStub psd = new PropertyStub(p, attributes);
props.Add(psd);
}
}
else
{
PropertyStub psd = new PropertyStub(p, attributes);
props.Add(psd);
}
}
}
PropertyDescriptor[] propArray = (PropertyDescriptor[])props.ToArray(typeof(PropertyDescriptor));
return new PropertyDescriptorCollection(propArray);
}
/// <summary>
///
/// </summary>
/// <param name="pd"></param>
/// <returns></returns>
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
#endregion
}
再接着我们在此类中,再生命一个类PropertyStub,让它继承于PropertyDescriptor(实现类上的属性抽象化,可以声明各种抽象概念的属性)具体详见下面代码,每个方法都有注明用途:
public class PropertyStub : PropertyDescriptor
{
/// <summary>
/// 发现属性 (Property) 的属性 (Attribute) 并提供对属性 (Property) 元数据的访问。
/// </summary>
readonly PropertyInfo info;
/// <summary>
/// 提供类的属性的抽象。
/// </summary>
/// <param name="propertyInfo"></param>
/// <param name="attrs"></param>
public PropertyStub(PropertyInfo propertyInfo, Attribute[] attrs)
: base(propertyInfo.Name, attrs)
{
info = propertyInfo;
}
/// <summary>
/// 获取用于获取此实例的类对象 MemberInfo。
/// </summary>
public override Type ComponentType
{
get { return info.ReflectedType; }
}
/// <summary>
/// 获取一个值,该值指示此属性是否只读。
/// </summary>
public override bool IsReadOnly
{
get
{
if (info != null)
{
MapControlAttibute uicontrolattibute = (MapControlAttibute)Attribute.GetCustomAttribute(info, typeof(MapControlAttibute));
if (uicontrolattibute != null)
return uicontrolattibute.IsReadOnly;
}
return info.CanWrite == false;
}
}
/// <summary>
/// 获取此属性的类型。
/// </summary>
public override Type PropertyType
{
get { return info.PropertyType; }
}
/// <summary>
///
/// </summary>
/// <param name="component"></param>
/// <returns></returns>
public override bool CanResetValue(object component)
{
return false;
}
/// <summary>
/// 用索引化属性的可选索引值返回指定对象的该属性值。
/// </summary>
/// <param name="component">将返回其属性值的对象。</param>
/// <returns>指定对象的属性值。</returns>
public override object GetValue(object component)
{
//Console.WriteLine("GetValue: " + component.GetHashCode());
try
{
return info.GetValue(component, null);
}
catch
{
return null;
}
}
/// <summary>
///
/// </summary>
/// <param name="component"></param>
public override void ResetValue(object component)
{
}
/// <summary>
/// 用索引化属性的可选索引值设置指定对象的该属性值。
/// </summary>
/// <param name="component">将设置其属性值的对象。</param>
/// <param name="value">新的属性值。</param>
public override void SetValue(object component, object value)
{
//Console.WriteLine("SetValue: " + component.GetHashCode());
info.SetValue(component, value, null);
}
/// <summary>
///
/// </summary>
/// <param name="component"></param>
/// <returns></returns>
public override bool ShouldSerializeValue(object component)
{
return false;
}
/// <summary>
/// 通过重载下面这个属性,可以将属性在PropertyGrid中的显示设置成中文
/// </summary>
public override string DisplayName
{
get
{
if (info != null)
{
MapControlAttibute uicontrolattibute = (MapControlAttibute)Attribute.GetCustomAttribute(info, typeof(MapControlAttibute));
if (uicontrolattibute != null)
return uicontrolattibute.PropertyName;
else
{
return info.Name;
}
}
else
return "";
}
}
}
自此,我们的属性基类已经定义完成,接下来,我们就可以开始定义我们自己主画面背景所需要的属性,比如背景颜色,背景大小,名称等…后续如果我们需要定义更多的其他属性,只需要在原来的基础上进行属性拓展即可。
我们现在开始声明背景的属性,我们添加一个类,命名为:MapPanelProperty,让他继承于PropertyBase
public class MapPanelProperty : PropertyBase
{
/// <summary>
/// 需要显示属性的控件
/// </summary>
private readonly MapPanel MapControl;
/// <summary>
/// 初始化
/// </summary>
public MapPanelProperty()
{
}
/// <summary>
/// 初始化
/// </summary>
/// <param name="MapControl">需要显示属性的控件</param>
public MapPanelProperty(MapPanel MapControl)
{
this.MapControl = MapControl;
}
[MapControlAttibute("地图名称", "地图名称", "", true)]
public string MapName
{
get { return MapControl.Name; }
set
{
MapControl.Name = value;
}
}
[MapControlAttibute("地图背景色", "地图背景色", "", false)]
public Color BackColor
{
get { return MapControl.BackColor; }
set
{
MapControl.BackColor = value;
}
}
[MapControlAttibute("地图大小", "地图大小", "", false)]
public Size Size
{
get { return MapControl.Size; }
set { MapControl.Size = value; }
}
}
由于颜色、名称、大小是控件基础属性,所以我们不需要在此委托和声明相应的事件,比如贝塞尔曲线的控制点坐标的变换,当触发事件时进行相应的操作(后面会介绍)。
最后,我们在工具当中就可以看到自己定义的组件(同我们平时Winform添加普通控件一样),将该组件添加到主画面当中,接着我们来看看选中背景时的效果,我们可以看到我们自己声明的属性以及对应的值,当更改其中任何一个属性值(对应的我们上面声明的类MapPanelProperty中的属性名)主页面背景会跟着变化(颜色或者大小等):
结尾
到这里我们的主背景页面的属性已经绑定完成了,下一节我们将站点的编辑(添加、移动、删除、保存、属性值绑定等操作)。