基于WPF实现的简单绘图工具

WhiteInterweaves

发布日期: 2019-04-02 10:31:11 浏览量: 600
评分:
star star star star star star_border star_border star_border star_border star_border
*转载请注明来自write-bug.com

1、系统功能设计

  • 开发、测试环境

    • 开发环境: Visual Studio 2012 Premium
    • 运行框架:.net framework 4.5
    • 测试环境:Windows 8.1、Windows 7
  • 开发语言

    • C#
    • XAML

1.1 前言

当初选择这么一个软件来编写纯粹是出于对玩弄文字游戏的xx管理系统的不喜,但我们没有料想到,经实践表明,涉及实时跟踪鼠标键盘事件和实时绘图的软件编写难度远大于主要通过文字实现信息交互的xx管理系统。仅仅实现一个屏幕上图形的框选功能就让我改了六七遍代码,我的队友更是间断地找出了五个bug。当终于能够把整体功能流畅地实现时,我们对软件开发者的了解与敬意又加深了一层。

1.2 总体功能描述

当今图像处理越发普及,人们对于图像处理的需求也各不相同。而一些绘图软件存在过于复杂(如PS)或是只具备基础功能(如windows自带画图)的问题,因此我们开发一个基于Windows Presentation Foundation(WPF)的简单绘图工具。

以下为程序的工作界面

1.3 功能点说明

功能类型 功能点名称 按键 实现方式 功能点描述
基本功能 主流类型图片载入 Ctrl+O 自己编写C#代码 支持.png.jpg.gif.bmp.eps等主流图片类型的读入和加载。使用C#标准OpenFileDialog对象获得文件路径并且使用Uri读入之后转为BitmapImage,用Image控件显示
基本功能 图片保存 Ctrl+S 自己编写C#代码 支持bmp(位图)和eps(矢量图)两种格式。使用SaveFileDialog对象获得文件路径之后用filestream输出。
基本功能 鼠标手绘图形 鼠标拖框 自己编写C#代码 包括矩形、圆角矩形、圆、椭圆、直线、贝塞尔曲线等
基本功能 单击选择图形 鼠标单击 自己编写C#代码 调用类接口SelectPoint来实现
复杂功能 框选图形 鼠标拖框 自己编写C#代码 递归调用类接口SelectRect来实现
复杂功能 全选图形 Ctrl+A 自己编写C#代码 调用类接口SelectAll来实现
复杂功能 多次选中图形 Shift+鼠标拖框 自己编写C#代码 调用类接口MergeComposite来实现
复杂功能 被选择图形的闪烁 自己编写C#代码 通过来回设置选择的CompositeGraphic的isVisible属性来达到闪烁的目的。
复杂功能 删除图形 Delete 自己编写C#代码 调用类接口Clear来实现
复杂功能 图形状态变更 自己编写C#代码 设置类属性DrawMode来实现。可以设置图形的边框粗细、颜色,以及内部颜色
复杂功能 调色板 使用第三方库 支持通过ARGB属性或是在图形中直接选色的方式给图形的边框和填充分别选色
复杂功能 拖动图形 鼠标按住拖动 自己编写C#代码 调用类接口Move来实现
复杂功能 键盘移动 Up,Down, Left,Right 自己编写C#代码 通过长按可快速移动
复杂功能 剪切选中图形 Ctrl+X 自己编写C#代码 将选中的图形加入一个List,并从画布上删除选中的图形
复杂功能 复制选中图形 Ctrl+C 自己编写C#代码 将选中的图形加入一个List,但不从画布上删除选中的图形
复杂功能 粘贴图形 Ctrl+V 自己编写C#代码 对上述List中的所有成员递归调用接口 ICloneable.Clone。长按Ctrl+V可以连续粘贴
统计功能 图形个数统计 自己编写C#代码 递归调用类属性Count并将它和Label绑定
用户体验优化 打开和新建图片、关闭程序时的友情提醒。 自己编写C#代码 防止误删未完成画布中的内容。
用户体验优化 显示系统时间 自己编写C#代码 使用DateTime.Now.ToString方法
错误处理 载入图像时对不支持图像及无法处理的图像的抛出。 自己编写C#代码 若捕获异常,会弹出对话框

2、系统总体结构

2.1 概要设计

按照“面向接口编程,而不是面向实现编程”的面向对象基本原则,在建立解决方案的时候我就将解决方案分成了Ccao-big-homework(UI,WPF工程项目,由我的队友邵键准同学实现,调用者)和Ccao-big-homework-core(实现,C#类库,由我实现,被调用者)。双方互不干涉,独立调试,在整个大作业过程中不曾因为接口耦合问题出现bug。

由于采用了多种科学合理的设计模式(见下文各模块介绍),本类库在维持良好的可复用性的同时,不曾进行过任何强制类型转换,不曾进行过任何运行时类型判定,所有的多态都靠重载函数来实现,充分体现了面向对象的思想。

3、具体实现

3.1 GUI的设计

GUI的设计采用了较为简洁的风格,设计完成后曾请求周边同学进行体验并对细节进行改进,力求用户体验较好。主体采用XAML语言,实现设计和功能的分离,并配之以C#的事件处理函数。程序取消了不怎么美观的窗口边框,并采取点住程序窗口任何一个位置均可拖动的方法。

3.1.1 程序启动和关闭动画设计

程序的主要窗口在开始和结束时都是通过淡入和淡出来呈现和销毁窗口,实现此效果使用了一个计时器,并让窗口的透明度随计时器而改变。下图为启动过程截图,可看见窗体还是半透明状态(请无视背景的代码)。

3.1.2 程序启动界面的设计

界面包括版本号与开发人员,“新建绘图”按钮在鼠标移上后会有高亮,点击进入绘图页面,点击“离开”按钮则直接调用Application.Current.Shutdown()函数销毁窗口。

3.1.3 程序主体绘图界面设计

主体绘图界面如下。

上部为菜单栏,具体按钮功能见使用手册,实现了鼠标移到某个按键上时该按钮闪烁一次并且放大,同时调整整个工作框的布局。效果如下。

左部为绘图框,选择后可使用鼠标绘制不同的图形,包括直线、圆、椭圆、正方形、长方形、贝塞尔曲线。其中贝塞尔曲线限于WPF提供的贝塞尔曲线构造函数的局限性,其必定从画布的左上角开始绘制。

左下角为一个图标,无实际作用。

3.1.4 程序图标的添加

我们的程序可是有图标的哦!

下面是调色板的图标。

3.2 逻辑层

主要实现解决方案Ccao-big-homework。

3.2.1 工作窗口WorkWindow部分

3.2.1.1 概述

主要实现人与程序的交互,包括鼠标事件和键盘事件等。由于绘图软件基本是在一个窗口进行操作,一般使用组合而非继承的方式,因此类的结构比较扁平。类图如下。

由于没有继承,全部实现在一个窗口里代码显得非常臃肿,因此我根据功能的不同将同一个窗口类分成了如下几个文件。

下面逐步介绍各个文件实现的方法。

3.2.1.2 BtnEvent

实现菜单栏所有按钮功能,包括新建、打开、保存、退出、全选、复制、剪切、粘贴、样式选择按钮。实现方法是调用其他文件里的私有函数。

3.2.1.3 FileEvent

实现图片读写的相关方法。我们的程序支持使用文件夹视图来把文件保存到计算机的任意位置或是从计算机任意位置载入图片。同时还在现有图片未保存时弹出对话框提示用户要保存,防止了图片的误删,优化用户体验。

实现画布的新建与刷新,并实现程序的退出功能。

3.2.1.4 FishEyePanel

实现上部菜单栏中当鼠标移到某个按键上时该按钮闪烁一次并且放大,同时调整整个工作框的布局。此处采用了组合的形式,FishEyePanel是一个新的类,在WorkWindow类中创建该类的对象,并布局到主窗口上。

3.2.1.5 GraphicsOperation

实现图像整体操作,包括:

  • 全选:调用队友提供的SelectRect接口,并把范围设置成整个画布大小的矩形,从而得到一个类型为List<CompositeGraphic>的对象,添加到selectedGraphics里

  • 复制:调用队友提供的Clone()函数,往clonedGraphics这个对象里添加对象

  • 剪切:复制的同时清空selectedGraphics

  • 粘贴:将clonedGraphics里的所有成员添加到总画布compositeGraphic的Children()成员中,从而实现绘制,并向右下角移动(10,10),从而和复制的原图区分开,然后清空clonedGraphics(),并再次调用复制函数(),从而实现粘贴的连续性,即复制一次可连续粘贴。下图为按下Ctrl+C后连续按下Ctrl+V的效果

3.2.1.6 KeyBoard

实现键盘的按键监控,包括:

  • Ctrl+A:全选图像

  • Ctrl+C:复制选中图像

  • Ctrl+V:粘贴选中图像

  • Ctrl+X:剪切选中图像

  • Ctrl+W:关闭程序

  • Ctrl+O:打开图片

  • Ctrl+N:新建画布

  • Ctrl+S:保存图片

  • Delete:删除选中图像

  • Key Up、Key Down、Key Left、Key Right:选中图像的上下左右移动

  • Shift:按住时可多次增加选择已选中的图形

3.2.1.7 MouseEvent

我的工作中最难的部分,而且肩负调试队友代码的使命。

  • Window_MouseLeftButtonDown事件:实现鼠标不在画布上时窗口根据鼠标的移动而拖动

  • OnMouseLeftButtonDown事件:鼠标左键按下处理事件。主要记录鼠标开始移动的点startPoint,让画布捕捉到鼠标,并标记左侧的ToolBar是否选中rbSelect选择按钮

  • OnMouseMove事件:鼠标移动时的处理事件。当鼠标移动且画布捕捉到鼠标的时候,若是发现rbSelect选择按钮被选中且当前有图形被选中且Shift键未被按下,那么说明这个鼠标事件需要的是移动图形,因而用虚直线实时绘制鼠标指示的移动路径;若是其他绘图按钮被选中,则说明要绘制图形,则用虚长方形或是虚直线实时绘制图形

  • OnMouseLeftButtonUp事件:鼠标左键抬起的处理事件。首先判断鼠标抬起时和按下时位置是否相同。若是相同,则说明用户只是按了一下,那么不管左侧ToolBar是选中的什么,说明用户都是想选中一个图像,因此将左侧的工具条调整到rbSelect按钮,并且调用总画布compositrGraphic的SelectPoint方法,得到这个点选中的图像,将其加入selectGraphics,在这个List里面的对象,每隔0.5秒更改一次可见性,从外观看来,选中的图形会闪烁。若是鼠标移动了,且选项选中的是图形选项卡,则绘制对应的图形,并且刷新画布,并把之前实时跟踪鼠标的虚线图形删去。接下来判断选项卡是否为贝塞尔曲线选项,若是则把该点增加到贝塞尔曲线的List里,当List里的成员个数增加到4时,绘制一条贝塞尔曲线并清空该List。最后是最为复杂的rbSelect按钮,如果当时选中的图形为0,即selectGraphics为空,则说明用户想要选中他框选的图形,则调用SelectRect得到选中的所有图形,并把其加入selectGraphics中使其闪烁。若selectGraphics不为空,则说明用户想要移动选中的图形,则调用move方法移动选中的图形,并把selectGraphics清空。同时,如果整个过程中Shift键被按下且rbSelect被选中,说明用户想要增加选择图形,于是将选中的图形增加到selectGraphics里面。

以上方法还各自特判了贝塞尔曲线绘制时的点四个点的情况。

3.2.1.8 Paint

各种图形的绘制,包括:

  • 直线

  • 长方形

  • 正方形

  • 椭圆

  • 圆角矩形

  • 贝塞尔曲线

其中(1)~(6)的绘制方式基本相同,都是新建一个该图形的对象,然后根据传来的两个点确定图形的长宽和位置,将这个对象添加至总画布compositeGraphic,然后刷新画布。

对于(7),绘制方式是在画布上点四个点,则出现贝塞尔曲线。

3.2.1.9 其他零碎功能

主要包括一些动画效果。启动时工具条的移入运用了ThicknessAnimation控件。为了美观我特意写了一个函数隐藏了工具条尾部的小箭头。选中图形闪烁的功能则使用了一个DispatcherTimer计时器,每过500ms就把selectGraphics的成员的可见性改变一次。图形个数统计调用总画布compositeGraphic的count属性,每隔0.5秒刷新一次。系统时间标签则使用DateTime.Now.ToString方法获取。

3.2.2 启动窗口MainWindow部分

这个窗口很简单,只实现了弹出WorkWindow和关闭窗口退出的功能。并贴了一张图,写了版本号。

3.2.3 颜色拾取窗口StyleSettingWindow部分

颜色拾取窗口我并没有花太多时间自己写,本来以为WPF自带调色板控件,结果发现没有,于是在网上找了一个扩展控件,并组合到WorkWindow类中,在按下上部菜单栏中的“样式选择”按钮时弹出。

3.3 类库Ccao-big-homework-core-wpf的实现

3.3.1概述。

本类库(Ccao-big-homework-core-wpf.csproj)基于WPF,文件统一注释为”Du 2015.9”。

由于本类库被设计作为实现图形操作的类的类库,所以各类之间比起“is-implemented-in-term-of-a“或者”has-a“来说更符合”is-a”关系,所以本类库大量使用了继承、多态、递归调用这些OO的方法,和UI相比更好的体现了这次大作业的教育目的。

3.3.2 MyGraphic类

基本的图像类。之所以声明为类而不是接口是因为有几个函数要有默认实现。

下图是Mygraphic类的全部类图。各函数基本都是函数名自解释的,如果一眼看不出来有什么用可以参见源代码的注释。

乍看上去,Ienumerator和Ienumerable两个接口(包括count,getenumerator,current,add,clear,dispose,reset等,可以使得这个类被像list一样用foreach遍历)看上去都不应该是MyGraphic类作为一个基本的图像类应该有的方法或者属性。事实上这里采用了Composite设计模式。为了使得单个对象和组合对象的使用具有一致性,其他库函数(以及用户)能够统一的使用组合结构中的所有对象(具体来说,使得我们不用在override各种函数的时候来写出诸如MyGraphic g as CompositeGraphic这种效率很低的运行时类型判定代码来),所以在MyGraphic类中定义了Composite类的各种接口,并且给以了默认实现(当然了由于MyGraphic不是CompositeGraphic,默认实现理应是空实现)。

另外,MyGraphic类还实现了接口ICloneable,这个接口允许MyGraphic被深复制,因而实现了UI的复制-粘贴功能。

IsVisible指示本图像是否会被画到画布上。

Father指示MyGraphic类的父图像。Father类的set方法被重写来调用父图像的DisposeChildren方法来删除父图像维护列表中自己的存在。这样就能保证整个MyGraphic类是一棵树(树的顶点是由UI保存的一个Composite,事实上充当了画布的功能),为递归遍历创造了条件。所以同时,当Father被置null的时候,本图形就会因为失去一切引用而被C#的GC机制自动回收,相当于是被Dispose了。因此本程序的效率大大提高,本人的电脑在有1500个图形的时候还能够正常工作。

SelectError指示点击一个点的时候允许多大的误差以选中一个图形。

还有不少函数在MyGraphic类中被声明仅仅是为了作为接口被递归调用,例如SelectRect、Count属性等就是这样,当然整个程序中最重要的Draw也不例外。

3.3.3 SingleModeGraphic类

顾名思义,是整个图形有同一个drawmode的类,也是所有基本图形(basic_graphics)的基类。显然composite类由于可以add进各种drawmode的图形所以不可能能维持有同一个drawmode。

drawmode指示一个给定border的geometry的图形如何被绘制,将调用drawmode的draw方法(见3.3.6)。

重载了基类的SelectRect方法,当选中时返回包括自己的一个CompositeGraphic,反之返回null。

getGeometry方法返回这个对象的border的geometry。

3.3.4 CompositeGraphic类

可以包含其他graphic的graphic的实现,或者说用windows画图打比方的话,就是那个选框功能。重载了IEnumerable和IEnumerator接口。

首先,作为Composite设计模式的一部分,override了MyGraphic中的Composite相关的函数(isComposite, Clear, Add, MoveNext, Reset, Current),进行了事实上的操作。

CompositeGraphic类维护一个私有的辅助list,用来保存每个成员相对于本体的左上角的相对位置。当CompositeGraphic类要执行某个操作的时候(例如Draw或者SelectRect),它就递归调用每个成员的相应操作,好像这个类不曾存在过一样(如果设置了isCombined = false,当这个标记被置true的时候整个CompositeGraphic将被视为是一个整体,类似于PPT或者Flash提供的“组合”功能)

另外CompositeGraphic也像SingleModeGraphic一样有drawmode类型的属性backgroundmode,这是为了画出它的边界和背景色。边界总是一个矩形。

当调用SelectRect的时候,由于递归调用的过程会产生大量很多层的CompositeGraphic(因为selectRect的原理是将所有被选中的图形改为连接到同一个Composite中,然后将这个Composite的父图形设为this),所以设计了MergeComposite,将一个Composite中的全部内容合并到另一个中,减少了递归的层次(在这种设计之下,递归最多两层),有效提高了运行效率。

3.3.5 基本的图像类(basic_graphics)

这些类都继承自SingleModeGraphic并且是Sealed类。有几个属性来记录自己的形状,并且override了getGeometry接口来确定自己的外形。如类图所显示的那样,新建一个基本的图像类是非常简单的,只需要实现getGeometry方法和Clone方法两个方法,并且定义几个足够确定图形位置和形状的属性,就能够被立刻应用到类库之中,像其他图形一样被选择、被移动、被更改颜色、被剪切、被复制、被粘贴,这充分体现了本类库良好的可扩充性。

对于Ellipse、Line、Rectangle,WPF都提供了相应的Geometry对象来绘画,然而对于Bezier,WPF并没有提供相应的Geometry,而是提供了一个PathSegment——BezierSegment。BezierSegment只有三个参数(也就是说默认从原点开始画),第四个点需要通过设置PathFigure的起始点来确定。

3.3.6 DrawMode类

抽象类。指示如何绘制一个Geometry形状的Mygraphic为drawing。这个类其实做成接口也没什么问题。如果深究起来的话把绘图方式单独封装起来似乎也是一种设计模式,学名叫做Strategy模式。Strategy模式的好处也是显而易见的——如果直接将各种绘图方式以硬编码的方式写在SingleModeGraphic中,虽然可以少几个类,但是这意味着你可以在完全不破坏类库的情况下添加新的DrawMode,使得它易于切换、易于理解、易于扩展,进一步体现了作为一个类库的重要理念——可扩充性。(嘛,不过这毕竟只是大作业,虽说类库要可扩充性良好,但是也就我自己扩充我自己的类库玩罢了…但是当我发现WPF的drawing系列图像的构架思想竟然和我一开始手画的类图一模一样的时候,我心中别提有多得意了)

3.3.7 DrawMode类的派生类

这些类都继承自Drawmode类并且是Sealed类。有几个属性来记录自己的绘图方式,且override了draw来绘制。

3.3.8 defaultConstant类

储存一些常量。在GDI+中有System.Drawing.Color枚举,然而在WPF中似乎没有对应字段,为了方便类库的设计所以作为static readonly变量定义在这里,效果类似于C++的全局const。另外准备了defaultbrush(透明色)和defaultpen(黑色,宽2.0f)来作为GeometryMode的默认构造值。

由于是常量,所以被作为partial类分散定义在drawmode的各个派生类中。

3.3.9 DrawingUIElement类

本来作为一个普适的类库有上面的类就已经能够绘图了;但是我所写的这个类库毕竟是个wpf类库,然而wpf的基本控件Canvas的Children只接受某种System.Windows.UIElement作为它的子成员。所以为了避免推倒重来,此处采用了Adapter(或者叫做Wrapper)设计模式,将本类库的接口类型(System.Windows.Media.Drawing)转换为我的调用方,邵键准同学的UI能够接受的参数UIElement。

重写了OnRender方法,只要对它调用UIElement所固有的InvalidateVisual方法就能迫使控件重画,从而将drawing指示的画面显示在wpf界面上。

3.4 类库Ccao-big-homework-core-winform的实现

本类库(Ccao-big-homework-core-winform.csproj,并没有被本次大作业引用)是一开始我的队友尚未开始施工的时候编写而成的(文件注释为”Du 2015.8”),当时前期调研做的不够好,没有意识到wpf和winform不兼容,所以手贱就先写了一个winform版。Winform版实现比wpf版要早,Class Diagram也和wpf版类似,几乎所有的类名都一样,只是winform版基于GDI+,命名空间为System.Drawing,而wpf版基于WPF,命名空间为System.Windows。在此对于两个类库之间雷同的部分不再赘述。

在本大作业中并没有用到winform类库(因为winform的界面远没有wpf美观),但是winform类库在我们往届的测试中证明是可以使用的(见github上9.6晚上的commit,hash为87ce7da,当时有一个基于VB的样例test基于winform测试了这个类库的可行性)。如果有兴趣做winform开发,可以尝试使用这个类库。

本类库和wpf版的唯一区别在于draw的实现。在wpf版中,draw函数是一个普通的返回System.Windows.Media.Drawing的函数。然而在winform中我们采取的并不是Adapter模式,而是另一种著名设计模式Bridge模式:我们有一个接口IWindow实现drawpath和fillpath两个接口,然后draw函数接受参数IWindow,并在函数体内调用这两个接口进行绘图,没有返回值。这样做的好处在于可以将抽象部分与它的实现分离,使得双方都可以有独立的变化。无论类库将要面对怎样的UI(即便这是个WPF UI甚至是个VB项目,例如我的测试用项目),只要这个UI实现了接口IWindow,类库就可以不作任何修改的应用到这个UI上。

以下是项目文件列表,除了IWindow外的所有类都和wpf版一样。

4、项目总结

4.1 亮点

首先,由于采用了Composite模式,所以几乎所有的Composite接口都是通过递归调用来完成的,简洁明了,体现了合理设计模式的优越性。假使不采用Composite模式,那么(任举一例),SelectRect方法将不得不返回一个list<MyGraphic>,且不说这样子将使得这个类库丧失组合/打散的固有功能,而且返回list将使得对于这个list的每个操作都必须由客户端通过类似foreach MyGraphic g…这样子的调用方式手动对每个list的成员执行。现在只需要调用Composite的对应函数,就能通过递归调用在对客户隐藏的情况下执行了,体现了良好的封装。

其次,本类库的可扩充性堪称良好。向本类库中加入新的几何图形只需要继承SingleModeGraphic并且重写getGraphic和Clone;向本类库中加入新的绘图方式只需要继承DrawMode并且重写Draw。

4.2 OOP模式的优越性

接口和实现的分离极大提高了我们的开发效率。只需要知道给接口提供什么参数,能得到什么结果就可以了,不需要知道它是如何被实现的,这样的黑箱极大地提高了我们的开发效率。

4.3 版本控制的重要性

版本控制不仅让我们能够清晰地了解自己的开发进度,而且在开发出现重大bug的时候,能够迅速地回滚到以前的版本。这样不会出现自己写了什么挂了都不知道的,结果只好全部删除重来的情况。况且,每一次在Git上的Commit都意味着我实现了新的功能,内心总能感到满足。

4.4 自主解决困难的能力

为了写大作业我估计百度了不下300次,基本开发的状态就是:想实现某功能—>发现自己不会—>百度google大法好—>会了—>实现该功能—>又想实现某功能。

开发过程中我遇到过很多困难,举个例子:关闭窗口时我写了个淡出动画,结果发现动画刚开始运行就程序就被退出,表现为动画无法播放。百度了一个小时,终于找到解决办法:在给程序的OnClosing事件中,把close命令取消,然后坐等0.6秒动画放完后再退出程序。

真正到开发程序才发现以前学的都是纸上谈兵,经过一份大作业的洗礼我收获了极大提升的编程技能。

上传的附件 cloud_download 基于WPF实现的简单绘图工具.7z ( 2.32mb, 20次下载 )
error_outline 下载需要12点积分

发送私信

别人早就看开的事情, 你只是比别人慢了点

5
文章数
11
评论数
eject