C#绘图程序结题报告
(一) 设计思路:
基于课上完成的绘图工具改进,实现几种图形,因在日常学习实验中涉及到UML图的设计与绘制,标识工具的使用频率较高,于是参照ProcessOn连线设置形式,在原有的基础上添加指示箭头图形与折线。
l 可绘制图形工具及其属性
1. 直线:粗细设置,箭头或普通样式
2. 指示箭头:大小设置
3. 折线:粗细设置,箭头或普通样式,线上设置锚点,可调节折角
4. 矩形:边框粗细,样式,描边或填充
5. 圆角矩形:边框粗细,样式,描边或填充,圆角半径
6. 椭圆类:边框粗细,样式,描边或填充
l 按下选择按钮后进入选择状态,产生选框与可拖拽锚点,中心点,旋转点,图形可进行移动,拉伸,旋转
l 可多选,全选,删除所选图形
l 边框与填充,二者颜色设置
l 清空画板,保存图像,打开图像,退出程序
使用C# 窗体应用+GDI, C#语言面向对象思想
将各种形状(线段、矩形、椭圆等)的起点坐标、大小、颜色等属性信息封装在内部程序。用Graphics类在panel上绘制,把所绘制的图形起点坐标、大小等数据记录在功能类中,使用户在控件上进行单击时,根据单击的坐标判断在矩形上单击或是空白处,在其坐标范围中,表示矩形被选中;用户进行移动鼠标操作,可则根据移动的距离重新计算矩形的起点坐标,得到重新绘制的图形。
将主要功能写入核心类,包括鼠标移动按下事件中的判断其所在位置,获取图形变换前后的坐标点,判断选中图形是否处于拖动选择状态,获取图像变化事件,最终图像,在选中图形的不同位置发生游标变化事件等。
为了程序显示清晰与功能的完整,使用System.Windows.Forms的Cursor类对不同状态下的鼠标指针样式进行设置;界面设计上对ToolStripLabel和ToolStripCombox的是否显示,对选中不同图形进行条件语句判断来设置。
寻找图形绘制属性的关系,对各种类进行继承关系的建立,重写方法,如:圆角矩形类、椭圆图形类继承于矩形类;填充描边类继承于描边类;直线、折线类继承于描边类。设定默认与各种控件传递来的属性。
实现图形变换功能,定义坐标点的有序对,根据不同图形性质,需定义2,3个成员变量,根据不同图形数学公式得到全部点的坐标数据,进行绘制与后续操作。
在绘制图形时,各个图形类中包含对轮廓和选中时的选框部分的绘制,选框与可拖拽锚点其成员变量的数据在类间得到传递。
(二) 核心代码:
https://gitee.com/clematisWSheena/drawing-program.git
Kernel.cs
包含了绘图所需功能
鼠标移动按下事件中的判断其所在位置,获取图形变换前后的坐标点,判断选中图形是否处于拖动选择状态,选择工具事件,获取图像变化事件,最终图像,在选中图形的不同位置发生游标变化事件等
创建涉及到绘画过程私有变量,在后边写入所需函数
private System.ComponentModel.EventHandlerList _events; private Bitmap finalBitmap;//结果图像 private Graphics graphics;//画板 private ToolCursorType cursorType;//使用的工具游标 private SelectToolsLocation selectToolInWhere;//位置 private bool isInSelecting;//判断是否选中 private bool mousePressed;//判断鼠标过程 private bool isEverMove;//判断 private List<DrawableBase> shapesList;//可选图形 private DrawableBase shapeInCreating;// private ToolType currentTool;//当前选择的绘图工具 private List<int> selectedShapesIndexList;//当前选择的图形 private Point lastMovePoint;//终点 private Point mouseDownLocation;//鼠标点 private int pressedHotSpotIndex;//选框 private PropertyCollector proCollector;//样式
选定图形后变更样式
private void applyNewProperty() { if (SelectedShapesCount < 1) return; // 当前仅支持一个选定的形状 if (SelectedShapesCount == 1) { DrawableBase shape = shapesList[selectedShapesIndexList[0]]; switch (shape.ShapeProperty.PropertyType) { case ShapePropertyType.IndicatorArrowProperty: shape.ShapeProperty = proCollector.GetIndicatorProperty(); break; case ShapePropertyType.StrokedProperty: shape.ShapeProperty = proCollector.GetStrokedProperty(); break; case ShapePropertyType.FilledProperty: shape.ShapeProperty = proCollector.GetFilledProperty(); break; case ShapePropertyType.RoundedRectProperty: shape.ShapeProperty = proCollector.GetRoundedRectProperty(); break; } } } private void CheckShapeProperty() { if (currentTool != ToolType.ShapeSelect || SelectedShapesCount != 1) return; bool diff = false; BaseProperty basepro = shapesList[selectedShapesIndexList[0]].ShapeProperty; if (basepro is IndicatorArrowProperty) { IndicatorArrowProperty ip = (IndicatorArrowProperty)basepro; if (ip.LineColor != proCollector.StrokeColor || ip.LineSize != proCollector.IndicatorLineSize) { diff = true; proCollector.StrokeColor = ip.LineColor; proCollector.IndicatorLineSize = ip.LineSize; } } if (basepro is StrokedProperty) { StrokedProperty sp = (StrokedProperty)basepro; if (proCollector.PenWidth != sp.PenWidth || proCollector.StrokeColor != sp.StrokeColor || proCollector.LineDash != sp.LineDash || proCollector.StartLineCap != sp.StartLineCap || proCollector.EndLineCap != sp.EndLineCap || proCollector.PenAlign != sp.PenAlign || proCollector.HowLineJoin != sp.HowLineJoin) { diff = true; proCollector.PenWidth = sp.PenWidth; proCollector.StrokeColor = sp.StrokeColor; proCollector.LineDash = sp.LineDash; proCollector.StartLineCap = sp.StartLineCap; proCollector.EndLineCap = sp.EndLineCap; proCollector.PenAlign = sp.PenAlign; proCollector.HowLineJoin = sp.HowLineJoin; } } if (basepro is FilledProperty) { FilledProperty fp = (FilledProperty)basepro; if (proCollector.PaintType != fp.PaintType || proCollector.FillColor != fp.FillColor || proCollector.FillType != fp.FillType) { diff = true; proCollector.PaintType = fp.PaintType; proCollector.FillColor = fp.FillColor; proCollector.FillType = fp.FillType; } } if (basepro is RoundedRectProperty) { RoundedRectProperty rp = (RoundedRectProperty)basepro; if (proCollector.RadiusAll != rp.RadiusAll) { diff = true; proCollector.RadiusAll = rp.RadiusAll; } } if (diff) { OnPropertyCollectorChanged(EventArgs.Empty); } }
// 图像改变事件 private static object Event_FinalBitmapChanged = new object(); public event EventHandler FinalBitmapChanged { add { _events.AddHandler(Event_FinalBitmapChanged, value); } remove { _events.RemoveHandler(Event_FinalBitmapChanged, value); } } protected virtual void OnFinalBitmapChanged(EventArgs e) { EventHandler handler = (EventHandler)_events[Event_FinalBitmapChanged]; if (handler != null) { handler(this, e); } } // 选中图形改变事件 private static object Event_SelectedShapesChanged = new object(); public event EventHandler SelectedShapesChanged { add { _events.AddHandler(Event_SelectedShapesChanged, value); } remove { _events.RemoveHandler(Event_SelectedShapesChanged, value); } } protected virtual void OnSelectedShapesChanged(EventArgs e) { CheckShapeProperty(); EventHandler handler = (EventHandler)_events[Event_SelectedShapesChanged]; if (handler != null) { handler(this, e); } } // 游标改变事件 private static object Event_CursorTypeChanged = new object(); public event EventHandler CursorTypeChanged { add { _events.AddHandler(Event_CursorTypeChanged, value); } remove { _events.RemoveHandler(Event_CursorTypeChanged, value); } } protected virtual void OnCursorTypeChanged(EventArgs e) { EventHandler handler = (EventHandler)_events[Event_CursorTypeChanged]; if (handler != null) { handler(this, e); } } // 选择工具事件 private static object Event_SelectToolInSelecting = new object(); public event EventHandler SelectToolInSelecting { add { _events.AddHandler(Event_SelectToolInSelecting, value); } remove { _events.RemoveHandler(Event_SelectToolInSelecting, value); } } protected virtual void OnSelectToolInSelecting(EventArgs e) { EventHandler handler = (EventHandler)_events[Event_SelectToolInSelecting]; if (handler != null) { handler(this, e); } } // 样式改变事件 private static object Event_PropertyCollectorChanged = new object(); public event EventHandler PropertyCollectorChanged { add { _events.AddHandler(Event_PropertyCollectorChanged, value); } remove { _events.RemoveHandler(Event_PropertyCollectorChanged, value); } } protected virtual void OnPropertyCollectorChanged(EventArgs e) { EventHandler handler = (EventHandler)_events[Event_PropertyCollectorChanged]; if (handler != null) { handler(this, e); } }
图形绘制属性的关系,对各种类进行继承关系的建立,重写方法,如:圆角矩形类、椭圆图形类继承于矩形类;填充描边类继承于描边类;直线、折线类继承于描边类。设定默认与传递来的属性
StrokedProperty.cs
public class StrokedProperty : BaseProperty //绘制样式属性 { public override ShapePropertyType PropertyType { get { return ShapePropertyType.StrokedProperty; } } public float PenWidth { get; set; } public Color StrokeColor { get; set; } public LineDashType LineDash { get; set; } public LineCapType StartLineCap { get; set; } public LineCapType EndLineCap { get; set; } public PenAlignment PenAlign { get; set; } public LineJoin HowLineJoin { get; set; } public StrokedProperty()//默认 { PenWidth = 1.0f; StrokeColor = Color.Black; LineDash = LineDashType.Solid;//线 StartLineCap = LineCapType.Square;//起始 EndLineCap = LineCapType.Square;//结束 PenAlign = PenAlignment.Center;//对齐方式 HowLineJoin = LineJoin.Round;//连接方式 } public StrokedProperty Clone() { StrokedProperty sp = new StrokedProperty(); sp.PenWidth = this.PenWidth;//粗细 sp.StrokeColor = this.StrokeColor;//边框颜色 sp.LineDash = this.LineDash;//线样式 sp.StartLineCap = this.StartLineCap;//起始 sp.EndLineCap = this.EndLineCap;//结束 sp.PenAlign = this.PenAlign;//对齐 sp.HowLineJoin = this.HowLineJoin;//连接方式 return sp; } }
StrokedShape.cs
继承于DrawableBase.cs
public abstract class StrokedShape : DrawableBase { private StrokedProperty _property; private Pen pen; public StrokedShape(Kernel container, StrokedProperty property) : base(container, property) { _property = property; if (_property != null) SetNewPen(); } protected override void AfterPropertyChanged(BaseProperty oldValue, BaseProperty newValue) { _property = (StrokedProperty)newValue; if (_property != null) SetNewPen(); } //设置样式 private void SetNewPen() { if (pen != null) pen.Dispose(); float width = Math.Min(_property.PenWidth, Const.Max_Stroke_Width); pen = new Pen(_property.StrokeColor, width); pen.Alignment = _property.PenAlign; pen.LineJoin = _property.HowLineJoin; //线样式 switch (_property.LineDash) { case LineDashType.Solid: pen.DashStyle = DashStyle.Solid; break; case LineDashType.Dot: pen.DashStyle = DashStyle.Dot; break; case LineDashType.DashedDot: pen.DashStyle = DashStyle.DashDot; break; case LineDashType.DashedDotDot: pen.DashStyle = DashStyle.DashDotDot; break; case LineDashType.Dash1: pen.DashPattern = new float[] { 2, 2 }; break; case LineDashType.Dash2: pen.DashPattern = new float[] { 4, 4 }; break; } //头部样式 switch (_property.StartLineCap) { case LineCapType.Rounded: pen.StartCap = LineCap.Round; break; case LineCapType.Square: pen.StartCap = LineCap.Square; break; case LineCapType.LineArrow: pen.CustomStartCap = new LineArrowCap(); break; case LineCapType.NormalArrow: pen.CustomStartCap = new AdjustableArrowCap(5, 5, true); break; case LineCapType.SharpArrow: pen.CustomStartCap = new SharpArrowCap(); break; case LineCapType.SharpArrow2: pen.CustomStartCap = new SharpArrowCap(8.0f, 6.4f, 4.2f); break; case LineCapType.Rectangle: pen.CustomStartCap = new RectangleCap(); break; case LineCapType.Circle: pen.CustomStartCap = new CircleCap(); break; case LineCapType.Hip: break; } //尾部样式 switch (_property.EndLineCap) { case LineCapType.Rounded: pen.EndCap = LineCap.Round; break; case LineCapType.Square: pen.EndCap = LineCap.Square; break; case LineCapType.LineArrow: pen.CustomEndCap = new LineArrowCap(); break; case LineCapType.NormalArrow: pen.CustomEndCap = new AdjustableArrowCap(5, 5, true); break; case LineCapType.SharpArrow: pen.CustomEndCap = new SharpArrowCap(); break; case LineCapType.SharpArrow2: pen.CustomEndCap = new SharpArrowCap(8.0f, 6.4f, 4.2f); break; case LineCapType.Rectangle: pen.CustomEndCap = new RectangleCap(); break; case LineCapType.Circle: pen.CustomEndCap = new CircleCap(); break; case LineCapType.Hip: break; }
FilledShape.cs
RectShape.cs
///<summary>
///结构体,用于表示一个矩形区域,可以是可拖动的线段顶点,虚线选择框上面锚点(拖动可对形状缩放)
///以及一个旋转形状的区域
///</summary>
publicstructDraggableHotSpot
{
public Rectangle Rect;//选框
public HotSpotType _type;//锚点类型
publicint AnchorAngle;//角度
publicbool Visible;//隐藏或显示属性
publicDraggableHotSpot(HotSpotType type)
{
Rect = Rectangle.Empty;
_type = type;
AnchorAngle = 0;
Visible = true;
}
publicDraggableHotSpot(Rectangle rect, HotSpotType type)
{
Rect = rect;
_type = type;
AnchorAngle = 0;
Visible = true;
}
public HotSpotType Type { get { return _type; } }
}
}
ShapeHelper.cs
//图形绘制实现工具,实现图形变换功能,定义坐标点的有序对,根据不同图形性质,需定义2,3个成员变量,根据不同图形数学公式得到全部点的坐标数据,进行绘制与后续操作
TransformHelper.cs
实现变换功能,根据鼠标移动位置和锚点坐标,数学公式计算得出图形变换后数据点
public static class TransformHelper { /// <summary> /// 用于椭圆变换,返回按旋转前x, y坐标放大后的椭圆的数据点 /// </summary> public static PointF[] GetScaledEllipsePathPoints(GraphicsPath path, int index, Point mousePos, bool shift) //路径中的点 { PointF[] pf = path.PathPoints; if (index > 3) { int[] ex = new int[] { 9, 0, 3, 6 }; return getScaledEllipsePathPoints_Sides(pf, ex[(index - 4)], mousePos); } else { return getScaledEllipsePathPoints_Corners(pf, index, mousePos, shift); } } private static PointF[] getScaledEllipsePathPoints_Corners(PointF[] pf, int index, Point mousePos, bool shift) //游标点 { int[] ex = new int[] { 6, 9, 9, 0, 0, 3, 3, 6 }; pf = getScaledEllipsePathPoints_Sides(pf, ex[index * 2], mousePos); return getScaledEllipsePathPoints_Sides(pf, ex[index * 2 + 1], mousePos); } private static PointF[] getScaledEllipsePathPoints_Sides(PointF[] pf, int index, Point mousePos) //边 { int index2 = (index + 6) % 12; float dx = mousePos.X - pf[index].X; float dy = mousePos.Y - pf[index].Y; double r = Math.Sqrt(dx * dx + dy * dy); double angle = Math.Atan2(pf[index].Y - pf[index2].Y, pf[index].X - pf[index2].X); double mouseAngle = Math.Atan2(mousePos.Y - pf[index].Y, mousePos.X - pf[index].X); double length = r * Math.Cos(mouseAngle - angle); dx = (float)(length * Math.Cos(angle)); dy = (float)(length * Math.Sin(angle)); int[] indices = new int[] { 9, 11, 12, 13, 15 }; foreach (int i in indices) { int j = (index + i) % 12; if (i == 11 || i == 12 || i == 13) { pf[j].X += dx; pf[j].Y += dy; } else { pf[j].X += dx / 2; pf[j].Y += dy / 2; } } dx = pf[index].X - pf[index2].X; dy = pf[index].Y - pf[index2].Y; length = Math.Sqrt(dy * dy + dx * dx); float len = (float)(length / 2 * Const.MagicBezier); angle = Math.Atan2(pf[index].Y - pf[index2].Y, pf[index].X - pf[index2].X); int t1 = (index + 8) % 12; int t2 = (index + 9) % 12; int t3 = (index + 10) % 12; pf[t3].X = (float)(pf[t2].X + len * Math.Cos(angle)); pf[t3].Y = (float)(pf[t2].Y + len * Math.Sin(angle)); pf[t1].X = (float)(pf[t2].X + len * Math.Cos(angle + Math.PI)); pf[t1].Y = (float)(pf[t2].Y + len * Math.Sin(angle + Math.PI)); int s1 = (index + 4) % 12; int s2 = (index + 3) % 12; int s3 = (index + 2) % 12; dx = pf[s2].X - pf[t2].X; dy = pf[s2].Y - pf[t2].Y; pf[s3].X = pf[t3].X + dx; pf[s3].Y = pf[t3].Y + dy; pf[s1].X = pf[t1].X + dx; pf[s1].Y = pf[t1].Y + dy; pf[12] = pf[0]; return pf; } /// <summary> /// 用于矩形变换,返回按旋转前x, y坐标放大后的数据点 /// </summary> public static PointF[] GetScaledRectPathPoints(GraphicsPath path, int draggedPointIndex, PointF draggedPoint, Point mousePos, bool shift) { if (draggedPointIndex > 3) return getRectScaledPoints_Sides(path, draggedPointIndex, draggedPoint, mousePos); else return getRectScaledPoints_Corners(path, draggedPointIndex, mousePos, shift); } private static PointF[] getRectScaledPoints_Sides(GraphicsPath path, int draggedPointIndex, PointF draggedPoint, Point mousePos) { int targetPt1Index = draggedPointIndex - 4; int targetPt2Index = (targetPt1Index + 1) % 4; int anglePt2Index = targetPt2Index; int anglePt1Index = (anglePt2Index + 1) % 4; PointF[] pf = path.PathPoints; double angle = Math.Atan2(pf[anglePt2Index].Y - pf[anglePt1Index].Y, pf[anglePt2Index].X - pf[anglePt1Index].X); float dy = mousePos.Y - draggedPoint.Y; float dx = mousePos.X - draggedPoint.X; double mouseAngle = Math.Atan2(dy, dx); double r = Math.Sqrt(dy * dy + dx * dx); double length = r * Math.Cos(mouseAngle - angle); pf[targetPt1Index].X = (float)(pf[targetPt1Index].X + length * Math.Cos(angle)); pf[targetPt1Index].Y = (float)(pf[targetPt1Index].Y + length * Math.Sin(angle)); pf[targetPt2Index].X = (float)(pf[targetPt2Index].X + length * Math.Cos(angle)); pf[targetPt2Index].Y = (float)(pf[targetPt2Index].Y + length * Math.Sin(angle)); return pf; } private static PointF[] getRectScaledPoints_Corners(GraphicsPath path, int draggedPointIndex, Point mousePos, bool shift) { int targetPt1Index = (draggedPointIndex + 3) % 4; int targetPt2Index = (draggedPointIndex + 1) % 4; int pinPtIndex = (draggedPointIndex + 2) % 4; PointF[] pf = path.PathPoints; double angle1 = Math.Atan2(pf[targetPt1Index].Y - pf[pinPtIndex].Y, pf[targetPt1Index].X - pf[pinPtIndex].X); double angle2 = Math.Atan2(pf[targetPt2Index].Y - pf[pinPtIndex].Y, pf[targetPt2Index].X - pf[pinPtIndex].X); float dy = mousePos.Y - pf[pinPtIndex].Y; float dx = mousePos.X - pf[pinPtIndex].X; double mouseAngle = Math.Atan2(dy, dx); double r = Math.Sqrt(dy * dy + dx * dx); double length1 = r * Math.Cos(mouseAngle - angle1); double length2 = r * Math.Cos(mouseAngle - angle2); float x1 = (float)(pf[pinPtIndex].X + length1 * Math.Cos(angle1)); float y1 = (float)(pf[pinPtIndex].Y + length1 * Math.Sin(angle1)); float x2 = (float)(pf[pinPtIndex].X + length2 * Math.Cos(angle2)); float y2 = (float)(pf[pinPtIndex].Y + length2 * Math.Sin(angle2)); pf[targetPt1Index].X = x1; pf[targetPt1Index].Y = y1; pf[draggedPointIndex].X = mousePos.X; pf[draggedPointIndex].Y = mousePos.Y; pf[targetPt2Index].X = x2; pf[targetPt2Index].Y = y2; return pf; } } }
StrokedProperty.cs
描边属性类,直线折线类
FormMain.cs
主页面功能显示与实现
标记索引值,用于后续事件传递
选择语句
修改事件
菜单栏设置
加载主窗体
菜单栏文件功能,编辑功能
调用Kernel类的方法
(三) 软件截图:
(四) 测试用例
1. 绘制直线,粗细为2,样式点线
2. 绘制直线,样式:线状箭头,实线,线状箭头
3. 绘制指示箭头,尺寸中
4. 绘制折线,粗细为6,红色,样式:圆角起点,实线,圆角终点
5. 绘制矩形,粗细为5,样式:圆角起点,实线,圆角终点,仅描边,颜色:蓝色
6. 绘制矩形,粗细为8,样式:圆角起点,虚线,圆角终点,
描边及填充,颜色:蓝色灰色
7. 绘制圆角矩形,仅填充,颜色:橙色
8. 绘制椭圆,粗细为14,仅描边,黑色,将其旋转90°
9. 移动图形从左到右
10. 拉伸图形
11. 将图像保存到桌面,文件名test1.jpg
12. 打开图像test1.jpg
13. 全选图形
14. 选中矩形并删除
15. 清空画板