基于python实现完整图形学功能系统
摘要 : 本次大作业已经完成。实现了一个完整的图形学系统,功能包括初始化/重置画布,保存画布,设置画笔颜色,绘制线段,绘制多边形,绘制椭圆,绘制曲线,对图元的平移、旋转、缩放和对线段的裁剪。系统实现了上述功能的算法、在 CLI 下使用文件输入调用上述功能的接口以及在 GUI 下使用鼠标事件进行绘图以及图元编辑的功能。
关键词 : 图形学系统;图元算法;文件输入;鼠标交互;GUI
一、语言、开发环境以及框架
本程序使用 python3.7 编写,在 windows 运行;图形界面部分使用 tkinter 框架。
二、数据结构
本程序的主要涉及到三种数据结构,第一个是用于存放图元信息的 Primitive 类及其派生类 Line,Circle,Polygon 等,第二个是 GUI 类,用于实现 GUI 交互逻辑,以及画板和图元信息的存放。第三个是 CLI 类,它是精简的 GUI 类,删去了和 GUI 相关的部分,保留绘图部分。
2.1 图元
所有的图元都是对象,他们的基类是 Primitive 类,包括以下数据属性:
python
definit(self,vertex,pno,color):
self.vertex=vertex
self.pixels=[]
self.pno=pno
self.color=color
- self.vertex list 类型,其中每个元素是一个二元组,表示该图元的顶点;
- self.pixels list 类型,元素类型同上,这个图元光栅化后的所有像素坐标都存在;
- self.pno int 类型,是图元的 id
- self.color 长度为 3 的 list, 表示这个图元的 RGB 值。
还包括以下方法:
- self.rasterization(self): 使用图元的顶点等信息,来求出该图元在画布中所有像素的位置,并将其存放在 self.pixels 数据属性中;
- self.get_pixels(self):返回 self.pixels;若为空,则先调用上一个函数进行光栅化然后再返回。
- self.get_color(self): 将 self.color 整理成一个 24 位的数字返回,供绘图使用。
·直线类:Line 继承自 Primitive 类, 还有以下数据属性:
```python definit(self,vertex,pno,method,color):
super().init(vertex,pno,color)
self.vertical=0#是否存在斜率
self.slope=0
self.method=method
```
- self.vertical int 类型,用来表示是否垂直;
- self.slope int 类型,是线段的斜率;若垂直,则将斜率定为 10000;
- self.method int 类型,用来表示画图的算法(DDA 或 Bresenham)
还有以下方法:
-
self.DDA(self): DDA 算法,用于在 self.rasterization(self)中调用。
-
self.Bresenham(self): Bresenham 算法,同上。
·椭圆类:Circle, 继承自 Primitives 类,还具有以下的数据属性:
```python definit(self,vertex,pno,color):
super().init(vertex,pno,color)
self.rx=vertex[1] [0]
self.ry=vertex[1] [1]
```
- self.rx, self.ry int 类型,长半轴的短半轴(ps. 椭圆的中心存放在 self.vertex 的第一个元素中)
·多边形类:Polygon,继承自 Primitive 类,还具有以下数据属性:
python
def __init__(self, vertex, pno, method, is_done, color): # is_done:命令行直接完成,图形界面要等待完成
super().__init__(vertex, pno, color)
self.method = method
self.is_done = is_done
self.lines = []
self.last_point = vertex[0]
self.is_updating = 0
self.new_point = [0, 0]
- self.method int 类型,用来表示画图的算法(DDA 或 Bresenham);
- self.is_done int 类型,用于区分命令行绘制还是鼠标绘制;
- self.lines list 类型,元素为 Line。用来表示构成多边形的直线;
- self.last_point, self.new_point, list 类型,self.is_updating int 类型,均用于鼠标绘制多边形过程中的操作。
还具有以下方法,均用于鼠标绘制多边形:
- self.updating(self, point): 鼠标拖动过程,用于动态显示当前绘制的边;
- self.update_rasterization(self, point):鼠标松开后,用于添加刚刚绘制好的顶点和边;
- self.done(self):完成绘制后,连接第一个和最后一个点。
·曲线类:Curve,继承自 Primitive 类,还具有以下数据属性
- self.alg 字符串,表示曲线绘制的算法,取值为‘Bezier’或‘B-spline’
2.2 GUI 框架
GUI 框架是 GUI 类的一个对象,包括以下数据成员:
·窗口以及组件
- self.top:主窗口
-
self.paper:tkinter 的画布
-
以及各种按钮:self.draw_line, self.draw_circle, self.clean, self.close, self.save, self.line_DDA , self.line_Bre, self.owl, self.polygon, 等等,用来输入坐标以绘制图元或者改变鼠标绘制图元的类型。
·图元相关信息
- self.primitives 由 Primitive 类的对象构成的 list,存放着画布中所有图元的信息
·画布相关信息
- self.image 图片文件,显示在 tkinter 的画布上
- self.color_r, self.color_g, self.color_b 画笔颜色
- self.size_x, self.size_y 画布大小
- self.save_name 要保存的文件名
- self.draw 画笔
等等。
以及各种运行时需要的函数。各函数的功能以及实现的过程将在框架设计部分详细描述。
2.3 CLI 类
CLI 类是进行文件输入和绘制的对象,包括以下数据成员:
- self.image 图片文件,显示在 tkinter 的画布上
- self.color_r, self.color_g, self.color_b 画笔颜色
- self.size_x, self.size_y 画布大小
- self.primitives 由 Primitive 类的对象构成的 list,存放着画布中所有图元的信息
- self.save_name 要保存的文件名
- self.draw 画笔
以及用来实现解析指令、绘制图元等功能的函数
三、图元绘制、变换算法原理
此部分基于 Primitive 类,图元的绘制使用图元的基本信息,如线段的顶点,椭圆的圆心的长短半轴,多边形的顶点等等,来获得图元的需要在画布上占据的所有像素点的坐标,并将其返回给框架。而图元的变换(待添加)在原图元的基础上,对图元进行修改并返回新的像素点坐标。
涉及算法有:
详细请参看Word文档
- 线段的 DDA 算法
DDA 算法方法是利用计算两个坐标方向的差 分来确定线段显示的屏幕像素位置的线段扫描转换算法。也就是说,通过在一个坐标轴 上以单位间隔对线段取样(取 △x=1 或 △y=1),计算 △y 或 △x 决定另一个坐标轴上靠近 线段路径的对应整数值。
- 线段的 Bresanham 算法
Bresenham 画线算法是一种精确而有效的光栅线段生成算法,它可用于圆和其它曲线显示的整数增量运算。
- 中点椭圆算法
椭圆被定义为到两个定点(焦点)的距离之和等于常数的点的集合。在任意方向指定一个椭圆的交互方法是输入两个焦点和一个椭圆边界上的点,利用这三个坐标位置,就可求出显式方程中的常数,而后就可求出隐式方程中的系数,并用来生成沿椭圆路径的像素。
- 多边形的绘制方法
多边形的绘制可以转变为多个直线的绘制,因此不涉及到新的算法。但是,由于鼠标绘制多边形的需要,多边形的某条直线可能并不属于这个多边形,这就需要多边形来提供更多的接口,来实现鼠标拖动时的预览。
- Bezier 曲线的绘制
Bézier 曲线是通过一组多边折线的各顶点唯一定义出来的。曲线的形状趋向于多边折线的形状,改变多边折线的顶点坐标位置和改变曲线的形状有紧密的联系。因此,多边折线有常称为特征多边形,其顶点称为控制顶点。一般,Bezier 曲线段可拟合任何数目的控制顶点。Bezier 曲线段逼近这些控制顶点,且它们的相关位置决定了 Bezier 多项式的次数。类似插值样条,Bezier 曲线可以由给定边界条件、特征矩阵或混合函数决定,对一般 Bezier 曲线,方便的是混合函数形式
- B 样条曲线的绘制
B 样条曲线使用 B 样条基函数代替 Bernstein 基函数,从而改进了 Bezier 特征多边形与 Bernstein 多项式次数有关,且是整体逼近的弱点。
- 图元的平移
平移是指将物体沿直线路径从一个坐标位置移动到另一个坐标位置的变换
- 图元的旋转
二维旋转是将物体沿 xy 平面内的圆弧路径重定位。
- 图元的缩放
二维缩放变换改变物体的尺寸。
- 线段的裁剪的 Cohen-Sutherland 算法
编码算法,即 Cohen-Sutherland 算法,是最早、最流行的线段裁剪算法。该算法采用区域检查的方法,能够快速有效地判断一条线段与裁剪窗口的位置关系,对完全接受或完全舍弃的线段无需求交,可以直接识别,大大减少求交的计算从而提高线段裁剪算法的速度。
- 线段的裁剪的梁友栋-Barsky 算法
线段裁剪的基本问题是:裁剪窗口是二维对象;而线段是一维对象,两个对象维数不同不便比较。因此,他们所提出的算法的解决思路是:将待裁剪线段及裁剪矩形窗口均看作点集,那么裁剪结果即为两点集的交集。
四、框架设计
本系统包括两个程序,分别使用文件输入进行绘图和使用鼠标交互进行绘图等操作,分别对应着 CLI 类和 GUI 类。在生成程序时,分别创建两个类中的一个类的一个对象,即可运行系统。本部分将详细描述各个部分的功能及其实现,并进行一些正确性、鲁棒性检测(待添加)以及效率分析。
4.1 CLI 设计
本部分以 CLI 类为基础,实现了通过文件输入进行绘图的功能。类的构造函数初始化了各个必要的数据成员,并将输入文件和保存路径作为参数传递给 cmd_line_act()函数(下 main 详述),后者将解析文件中的指令并进行操作。
4.2 命令行文件的读取和执行
CLI.refresh()函数:
此函数作为刷新函数,在添加了新的图元或者对图元进行了修改过后被调用,功能是清除画布并重新逐个绘制图元。
python
def refresh(self):
self.image = Image.new("RGB", (self.size_x, self.size_y), (255, 255, 255))
self.draw = ImageDraw.Draw(self.image)
i = 0
for primitive in self.primitives:
pixels = primitive.get_pixels()
for point in pixels:
if (point[0] >= 0) and (point[1] >= 0) and (point[0] <= self.size_x-1) and (point[1] <= self.size_y-1):
self.draw.point((point[0], point[1]), fill=primitive.get_color())
i = i + 1
CLI.cmd_line_act()函数:
在此函数中,逐行读取文件中的指令,并将指令进行分割,
python
forcmdincmd_file.readlines():
color=[self.color_r,self.color_g,self.color_b]
words=cmd.split()
并根据指令,对画布进行相应的操作:
- ·resetCanvas 指令:直接修改 self.size_x 和 self.size_y, 然后调用清空画布函数 self.clean_pic(self);
- ·saveCanvas 指令:将保存文件名与保存路径拼接,然后调用 self.save_canvas(self)函数;
-
·setColor 指令:直接修改 self.color_r, self.color_g,self.color_b 三个数据属性;
-
·drawLine,drawEllipse 指令:利用得到顶点等信息,创建一个 Line/Circle 类的对象,并将其放入 self.primitives 列表中,然后调用 self.refresh()对画布进行刷新操作;
python
elifwords[0]=="drawLine":
vertex=[[int(words[2]),int(words[3])],[int(words[4]),int(words[5])]]
pid=int(words[1])
alg=1ifwords[6]=="DDA"else2
line_2b_drawn=Line(vertex,pid,alg,color)
self.primitives.append(line_2b_drawn)
self.refresh()
- ·drawPolygon,drawCurve 指令:这两个指令相对特殊,由两行组成,因此在循环中,当前行为这两个操作的时候,要修改一个临时变量 lines,以方便下次循环读取多边形和曲线的点。此外,还需要几个临时变量来存储点的数量和图元 id 以及绘图算法:
python
elifwords[0]=="drawPolygon":
lines=1
line2_id=int(words[1])
line2_n=int(words[2])
line2_alg=1ifwords[3]=="DDA"else2
在下一次循环时的操作就和画直线和椭圆的操作相同了:
python
elif lines == 1: # polygon
print("Polygon, 2nd line")
vertex = []
for i in range(line2_n):
point = [int(words[2*i]), int(words[2*i+1])]
vertex.append(point)
polygon_2b_drawn = Polygon(vertex, line2_id, line2_alg, 1, color)
self.primitives.append(polygon_2b_drawn)
self.refresh()
lines = 0
- ·drawCurve 指令操作类似,略去具体代码。
- ·translate 指令:得到指令中的信息后,调用 Primitive 类的 translate 方法即可。
c++
elif words[0] == "translate":
print("translate")
for i in range(len(self.primitives)):
if int(words[1]) == self.primitives[i].get_id():
# num = i
self.primitives[i].translate(int(words[2]), int(words[3]))
break
self.refresh()
- ·rotate 指令:得到指令中的信息后,调用 Primitive 类的 rotate 方法即可。
c++
elif words[0] == "rotate":
print("rotate")
for i in range(len(self.primitives)):
if int(words[1]) == self.primitives[i].get_id():
# num = i
self.primitives[i].rotate(int(words[2]), int(words[3]), int(words[4]))
break
self.refresh()
·scale 指令:得到指令中的信息后,调用 Primitive 类的 scale 方法即可。
c++
elif words[0] == "scale":
print("scale")
for i in range(len(self.primitives)):
if int(words[1]) == self.primitives[i].get_id():
self.primitives[i].scale(int(words[2]), int(words[3]), int(words[4]))
break
self.refresh()
·clip 指令:得到指令中的信息后,调用 Primitive 类的 clip 方法即可。
c++
elif words[0] == "clip":
print("clip")
for i in range(len(self.primitives)):
if int(words[1]) == self.primitives[i].get_id():
self.primitives[i].clip(int(words[2]), int(words[3]), int(words[4]), int(words[5]), words[6])
break
self.refresh()
4.3 GUI 设计
GUI 分为菜单栏、按钮和画布。菜单栏中的“文件”包括清除画布、关闭程序以及调用上面部分的文件输入功能。“绘图”中可以选择直线、多边形、曲线的绘制算法以及线段的裁剪算法。
按钮包括了绘制各种图元、进行图元变换、打开命令行文件、清除画布以及退出程序的按钮。按下大部分按钮会调用 GUI.set_type()函数(下文详述)以改变当前鼠标的功能,而调色板会调用 windows 提供的调色版以改变画笔颜色,打开文件则会弹出窗口以选择文件和输出路径,清空画布和退出程序分别调用相关的函数。
画布是主体部分,鼠标交互在此中进行,白色部分为当前的画布,拖动白色部分的边缘可以调节画布大小,最大至填充满灰色区域。整个灰色部分为一个 tkinter.canvas 画布,而白色部分为一个 PIL.Image 对象。
GUI 如下图:
4.4 鼠标交互进行图元绘制及变换
首先是一些绘图共用接口:
·GUI.refresh(self): 用于在图中的部分图元有变动的时候,刷新画布。代码如下。
```python defrefresh(self): self.paper.delete(ALL) self.image=Image.new("RGB",(self.size_x,self.size_y),(255,255,255)) self.draw=ImageDraw.Draw(self.image) self.map=np.full((self.size_x,self.size_y),-1) i=0 for primitive in self.primitives: pixels=primitive.get_pixels() for point in pixels: if(point[0]>=0)and(point[1]>=0)and(point[0]<=self.size_x-1)and(point[1]<=self.size_y-1): self.draw.point((point[0],point[1]),fill=primitive.get_color()) if self.mappoint[0]]==-1: self.mappoint[0]]=i else: self.mappoint[0]]=-2 i=i+1 self.photo=ImageTk.PhotoImage(self.image) self.paper.create_image(2,2,image=self.photo,anchor=NW) if self.rotate_point!=[-1,-1]: self.paper.create_oval(self.rotate_point[0]-2,self.rotate_point[1]-2, self.rotate_point[0]+2,self.rotate_point[1]+2,fill='red')
if self.scale_point!=[-1,-1]:
self.paper.create_oval(self.scale_point[0]-2,self.scale_point[1]-2,
self.scale_point[0]+2,self.scale_point[1]+2,fill='green') ```
略去了部分后文中在特殊情况会使用的代码。首先抛弃旧的图片,创建新的图片。然后将 self.primitives 里的图元按照顺序,逐个获取其所有像素及其颜色,绘制在新的图片上。最后将新的图片粘贴在 canvas 上。
使用此方法刷新的效率理论上不高,尤其是在使用鼠标绘制图形的时候看似会有很大的延迟,不过经过测试,在画布中的线画图元数量小于 80 个时,能够保持每秒以上 30 帧的刷新率;而在接受效率损失之后,在对图元进行修改时,重叠部分的表现会更加准确:如,一个覆盖着其他图元的图元被裁剪掉覆盖着其他图元的一部分后,原先被覆盖的图元的这部分能够正确的显示;而在鼠标绘制图元时,在鼠标拖动的时候也能够方便的实现图元的预览。
·GUI.clean_pic(self): 用于清除画布并重新设置画布大小。首先清空 self.primitives,并清空 self.paper(即图片所挂靠的 canvas),然后调用 self.refresh()函数,根据尺寸设置新的画布。以及重置旋转中心、缩放中心等等。
```python defclean_pic(self):
self.paper.delete(ALL)
self.primitives=[]
self.rotate_point=[-1,-1]
self.scale_point=[-1,-1]
self.primitive_changing=-1
self.refresh()
self.is_curve_painting=0
self.is_polygon_painting=0
self.is_rotating=0
```
·GUI.save_canvas(self):使用 PIL.Image.save()函数来保存当前绘制的图片。
下面是绘图部分。
首先,定义三个鼠标的函数
```python defleftdown(self,event):
defleftmove(self,event):
defleftrelease(self,event): ```
并在__init__()函数中将其与画板(self.paper)绑定:
```python
self.paper.bind('
self.paper.bind('
self.paper.bind('
另外,GUI 类中还有几个转为鼠标绘制而准备的数据属性,其中,self.cur 为正在绘制的图元的 id,其数值等于 self.primitives 的长度;self.start_x, self.start_y 为按下鼠标时记录的坐标,self.type 为正在绘制的类型。
· 绘图种类设置
首先,数据属性中添加几个按钮,并挂在主窗口中。其作用是选择要使用鼠标绘制的种类。self.line_DDA=Button(self.top,command=lambda:self.set_type(0),text="DDA 直线")
```python self.line_Bre=Button(self.top,command=lambda:self.set_type(1),text="Bresenham直线")
self.owl=Button(self.top,command=lambda:self.set_type(2),text="椭圆")
self.polygon=Button(self.top,command=lambda:self.set_type(3),text="多边形") ```
其中,set_type 函数如下
python
defset_type(self,type_t):#设置鼠标画图的类型
self.type=type_t
if type_t>=4 and type_t!=8:
self.pack_dis_ctrl_point(1)
else:
self.pack_dis_ctrl_point(0)
if self.is_polygon_painting==1:#完成多边形的绘制
self.is_polygon_painting=0
self.primitives[self.cur].done()
self.refresh()
if self.is_curve_painting==1:
self.is_curve_painting=0
self.refresh()
if self.is_rotating:
self.is_rotating=0
self.rotate_point=[-1,-1]
self.refresh()
if self.is_scaling:
self.is_scaling=0
self.scale_point=[-1,-1]
self.refresh()
if self.is_clipping:
self.is_clipping=0
self.last_point=[-1,-1]
self.clip_point=[-1,-1]
self.refresh()
更改 self.type 数据属性,0 和 1 分别为 DDA 算法和 Bresenham 算法的直线,2 为椭圆,3 为多边形,4 为曲线,5 为平移变换,6 为旋转变换, 7 为缩放变换, 8 为裁剪。特殊的,如果正在绘制多边形、曲线或者正在进行旋转或者缩放变换的话,点击选择种类的按钮会使多边形和曲线绘制停止、删除当前的旋转或缩放中心并连接多边形第一个和最后一个点。
· 直线的绘制
按下鼠标左键,会开始记录第一个坐标, 并以此坐标为起点和终点,在画布中创建一条直线,最后刷新画布。
```python defleftdown(self,event):
self.cur=self.primitives.len()#为即将要绘制的图元分配图元 id
self.start_x=event.x#记录第一个点的坐标
self.start_y=event.y
color=[self.color_r,self.color_g,self.color_b]
if self.type==0orself.type==1:
temp_list=[[self.start_x,self.start_y],[self.start_x,self.start_y]]
line_being_drawn=Line(temp_list,self.primitives.len(),self.type+1,color)
self.primitives.append(line_being_drawn)
```
拖动鼠标左键,更新当前正在绘制的直线的两个点,第一个点是鼠标左键点击的时候记录下的点,第二个点是当前的点,重新计算图元的像素,并刷新画布,以实现拖动的过程中能够预览的效果。
```python defleftmove(self,event):
x=event.x
y=event.y
if self.type==0orself.type==1:
temp_list=[[self.start_x,self.start_y],[x,y]]#更新当前正在绘制的图元的点
self.primitives[self.cur].vertex=temp_list
self.primitives[self.cur].rasterization()
```
松开鼠标,结束绘制,无需进行操作。
· 椭圆的绘制 :基本和直线类似
按下鼠标左键,开始画图;拖动鼠标时更新圆心坐标以及长半轴短半轴,并重新生成图元的像素;松开鼠标左键,结束绘制。
· 多边形的绘制
多边形的绘制相对复杂,本程序多边形的绘制逻辑与 windows 画图程序类似,即,按下并拖动形成第一条直线,接下来点击或拖动形成后续的直线,最后按选择画图类型的按钮结束绘制并形成封闭多边形。
首先,需要 GUI 类的两个数据属性,self.is_polygon_painting 和 self.polygon_last_point,分别记录当前是否正在绘制多边形以及多边形上一个画过的点。
按下鼠标左键时,分为两种情况:若当前没有正在绘制多边形,则创建一个新的多边形,其中 is_done 参数为 0,然后 self.is_polygon_painting 设置为 1。
若正在绘制,由于函数开头的语句将 self.cur 增加了 1,因此要先将其减一,然后用当前点对多边形进行 update。
python
elif self.type==3:
if self.is_polygon_painting==0:polygon_being_drawn =
Polygon([[self.start_x,self.start_y]],self.primitives.len(),1,0,color)
self.primitives.append(polygon_being_drawn)
self.is_polygon_painting=1
else:
self.cur-=1
self.primitives[self.cur].updating([event.x,event.y])
鼠标拖动时,只需要对使用当前点对图像进行 update,此时多边形的一条边尚未绘制完成,拖动时经过的点不能加入多边形,只是显示出来以供预览,因此要使用 updating()函数而非 update_rasterization()函数。刷新画布。
python
elif self.type==3:
self.primitives[self.cur].updating([x,y])
松开鼠标左键,鼠标所在位置将被添加进多边形中,多边形的一条边绘制完毕。
python
elif self.type==3:
self.primitives[self.cur].update_rasterization([event.x,event.y])
self.polygon_last_point=[event.x,event.y]
self.refresh()
结束绘制多边形:任意点击一个选择类型即可。此功能在 GUI.set_type(self, type)函数中。
python
if self.is_polygon_painting==1:#完成多边形的绘制
self.is_polygon_painting=0
self.primitives[self.cur].done()
self.refresh()
· 曲线的绘制
和多边形的绘制有些类似,
点击鼠标左键,若当前没有进行绘制,则创建一个以当前点为两个控制点的曲线;若当前曲线正在绘制,则调用 Curve.begin_update()方法将当前的点添加进曲线的控制点中。
python
elif self.type==4:#curve
if self.is_curve_painting==0:
curve_being_drawn=Curve([[self.start_x,self.start_y],[self.start_x,self.start_y]],self.primitives.len(),('Bezier'ifself.curve_type==1else'B-spline'),0,color)
self.primitives.append(curve_being_drawn)
self.is_curve_painting=1
else:
self.cur-=1
self.primitives[self.cur].begin_update([event.x,event.y])
self.refresh()
拖动鼠标左键,调用 Curve.updating()方法,将控制点中的的最后一个,也就是刚刚拖动经过的点修改为现在的点,并对图元进行刷新。
python
elif self.type==4:
self.primitives[self.cur].updating([x,y])
松开鼠标左键,同拖动,无需进行操作,当前点会永久添加进入当前正在绘制的曲线的控制点中。
python
elif self.type==4:
self.primitives[self.cur].updating([event.x,event.y])
self.refresh()
· 图元的平移
对于使用鼠标事件实现图元的变换,这里需要在 GUI 类中添加新的数据属性 self.map,是一个二维矩阵,大小和画布大小相同,矩阵中的元素存放着占用当前点的图元的下标,若没有被占用则为-1,若被多个图元占用则为-2,self.map 的更新在 self.refresh()函数中,即每次刷新都要更新 self.map 矩阵。这样的矩阵可以使得鼠标事件来实现图元的变换成为可能。
另外,在鼠标左键点击的函数中定义一个 find()函数,用来确定当前的鼠标位置对应着哪一个图元。由于很难实现精准的点击,因此将误差设置为四周的五个像素。函数实现如下:
python
def find(point):
res=-1
for i in range(point[0]-5,point[0]+5):
for j in range(point[1]-5,point[1]+5):
if(i>=0)and(i<=499)and(j>0)and(j<=499):
if self.mapi>=0:
if res==-1:
res=self.mapi
elif self.mapi!=res:
res=-1
return res
print("select",res)
return res
鼠标事件实现图元的平移本身比较简单,鼠标左键点击时,选择图元,若未找到则无响应,若找到了则将正在移动的图元设置为此图元。
python
elif self.type==5:
self.primitive_changing=find([event.x,event.y])
if self.primitive_changing>=0:
self.is_translating=1
self.last_point=[event.x,event.y]
鼠标拖动时,调用 Primitives 类的 translate 函数,计算当前坐标与上一个坐标的差值并进行移动。
python
elif self.type==5andself.is_translating==1:
self.primitives[self.primitive_changing].translate(x-self.last_point[0],y- self.last_point[1])
self.last_point=[x,y]
鼠标松开时,结束平移。
python
elif self.type==5:
self.is_translating=0
self.primitive_changing=-1
· 图元的旋转
首先,第一次点击时,点击一个位置来选择旋转中心,当再次选择了绘图或者变换类型时,旋转中心会被删除。然后后续操作与平移类似。鼠标左键点击时, 选择图元并记录当前角度。需要注意的时 arctan 函数的定义域和值域,所以会函数中考虑了一些特殊情况。另外,在选择图元之后,还调用了 Primitive.change()函数,此函数将图元的顶点以浮点数记录,可以显著提高连续旋转以及缩放时的精度。
python
elif self.type==6:
print("rotate")
if self.is_rotating==0:
self.rotate_point=[event.x,event.y]
self.is_rotating=1
self.refresh()
else:
self.primitive_changing=find([event.x,event.y])
if self.primitive_changing>=0:
#self.is_rotating=1
self.primitives[self.primitive_changing].change(1)
if event.x==self.rotate_point[0]:
self.start_angle=math.pi/2ifevent.y-self.rotate_point[1]>0else-math.pi/2
else:
self.start_angle=math.atan((event.y-self.rotate_point[1])/(event.x-self.rotate_point[0]))
if event.x-self.rotate_point[0]<0:
self.start_angle=self.start_angle+math.pi
鼠标拖动时,计算当前角度和前一个点的差值调用 Primitive.rotate()函数来进行旋转,并记录当前角度。
```python elif self.type==6andself.primitive_changing!=-1: if x==self.rotate_point[0]: angle=math.pi/2ify-self.rotate_point[1]>0else-math.pi/2 else: angle=math.atan((y-self.rotate_point[1])/(x-self.rotate_point[0])) if x-self.rotate_point[0]<0: angle=angle+math.p self.primitives[self.primitive_changing].rotate(self.rotate_point[0],self.rotate_point[1],
(angle-self.start_angle)*180/math.pi)
self.start_angle=angle ```
鼠标松开时,结束旋转,并调用 Primitive.change()函数删除图元中以浮点数存放的顶点。
python
elif self.type==6:
self.primitive_changing=-1
self.primitives[self.primitive_changing].change(0)
· 图元的缩放
图元缩放的实现与旋转基本相同,鼠标左键点击时,先选择一个点作为缩放中心,然后再选择图元进行缩放。
python
elif self.type==7:
if self.is_scaling==0:
self.scale_point=[event.x,event.y]
self.is_scaling=1
self.refresh()
else:
self.primitive_changing=find([event.x,event.y])
self.primitives[self.primitive_changing].change(1)
if self.primitive_changing>=0:
print('a')
self.start_distance=math.sqrt(pow(event.x-self.scale_point[0],2)+pow(event.y-self.scale_point[1],2))
鼠标移动时,计算差值、缩放并记录当前的缩放倍数。
python
elif self.type==7andself.primitive_changing!=-1:
cur_dis=math.sqrt(pow(event.x-self.scale_point[0],2)+
pow(event.y-self.scale_point[1],2))
self.primitives[self.primitive_changing].scale(self.scale_point[0],self.scale_point[1],
cur_dis/self.start_distance)
self.start_distance=cur_dis
鼠标松开时,结束缩放并调用 Primitive.change()函数。
python
elif self.type==7:
self.primitive_changing=-1
self.primitives[self.primitive_changing].change(0)
· 图元的裁剪
第一次点击时,选择一个图元。若被选择的图元是直线,则在图元两端进行标记。(GUI。refresh()函数中):
python
if self.type==8andself.primitive_changing!=-1andself.primitives[self.primitive_changing].__class__.__name__=='Line':
vertex=self.primitives[self.primitive_changing].get_vertexes()
self.paper.create_oval(vertex0-2,vertex0-2,vertex0+2,vertex0+2,fill='blue')
self.paper.create_oval(vertex1-2,vertex1-2,vertex1+2,vertex1+2,fill='blue')
第二次点击下时,记录下裁剪区域的第一个顶点。拖动时,得到裁剪区域的另一个顶点。创建一个与需要被裁剪的直线相同的临时直线,对其进行裁剪。
```python elif self.type==8andself.primitive_changing!=-1andself.is_clipping==1:
self.clip_point=[x,y]
self.tmp_cut_line=Line(self.primitives[self.primitive_changing].get_vertexes(),-1,
self.primitives[self.primitive_changing].get_method(),0) print(self.primitives[self.primitive_changing].get_vertexes()) self.tmp_cut_line.clip(self.last_point[0],self.last_point[1],self.clip_point[0],self.clip_point[1],self.clip_alg) self.clipped=1 ```
在 GUI.refresh()函数中刷新时,高亮显示临时直线的裁剪效果,并用虚线绘制当前的裁剪区域。
python
if self.type==8andself.primitive_changing!=-1andself.is_clipping==1:
vertex=self.tmp_cut_line.get_vertexes()
if self.tmp_cut_line.is_deleted!=1:
self.paper.create_line(vertex0,vertex0,vertex1,vertex1,fill='red',width=3)
vertex=[self.last_point,self.clip_point]
xmax=max(vertex0,vertex1)
xmin=min(vertex0,vertex1)
ymax=max(vertex0,vertex1)
ymin=min(vertex0,vertex1)
x=[xmin,xmin,xmax,xmax]
y=[ymin,ymax,ymax,ymin]
for i inrange(4):
self.paper.create_line(x[i],y[i],x[(i+1)%4],y[(i+1)%4],fill='red',dash=(4,4))
松开鼠标时,即即对线段本身进行裁剪。
·线段裁剪实现效果:
总体实现效果:
五、其他功能
5.1 拖动画布边界以调整画布尺寸
当鼠标按下时,若按下坐标位于边界附近时,开始调整画布大小:
python
if self.size_x-5<=event.x<=self.size_x+5andself.size_y-5<=event.y<=self.size_y+5:
self.is_image_scaling=3
elif self.size_x-5<=event.x<=self.size_x+5:
self.is_image_scaling=1
elif self.size_y-5<=event.y<=self.size_y+5:
self.is_image_scaling=2
拖动时,实时更新画布大小并重新绘制现有图元:
python
if self.is_image_scaling>0:
self.cur-=1
if self.is_image_scaling==1:
self.size_x=1000ifx>=1000elsex
elif self.is_image_scaling==2:
self.size_y=600ify>=600elsey
else:
self.size_x=1000ifx>=1000elsex
self.size_y=600ify>=600elsey
self.set_type(self.type)
松开时,结束缩放
python
if self.is_image_scaling>0:
self.is_image_scaling=0
5.2 显示曲线控制点并随时拖动控制点以调节曲线
为了能更明确的表示曲线,精准控制曲线的位置,以及体现曲线的形式,增加了显示曲线的控制点和拖动控制点以实现调整曲线的功能。
显示:
GUI.refresh()函数中,增加如下代码:
python
if self.type>=4 and self.display_ctrl_point==1:#绘制曲线控制点和虚线
self.map_ctrl_point=np.full((self.size_x,self.size_y,2),-1)
for i inrange(self.primitives.len()):
if self.primitives[i].class.name=='Curve':
vertex=self.primitives[i].get_vertexes()
for j inrange(vertex.len()):
self.paper.create_oval(vertexj-2,vertexj-2,vertexj+2,vertexj+2)
if 0<=vertexj<=self.size_x-1and0<=vertexj<=self.size_y-1:
self.map_ctrl_pointvertex[j][0][1]]=[i,j]
if j>=1:
self.paper.create_line(vertexj-1,vertexj-1,vertexj,vertexj,fill='red',dash=(4,4))
这样,在勾选显示控制点且正在绘制曲线或图元变换时,可以显示控制点。效果如图:
变换:
在选择绘制曲线或者图元变换时,可以选择显示或者隐藏控制点:
为此在 GUI 类中增加了以下成员:
```python self.display_ctrl_point=0
self.ctrl_point_check=Checkbutton(self.top,text="显示控制点",command=self.change_dis_ctrl_point)
self.map_ctrl_point=np.full((self.size_x,self.size_y,2),-1) self.is_curve_modifying=0 self.curve_modify_num=[-1,-1] ```
以显示勾选框和调整的图元下标、以及用来指示选择的位置是哪个图元的第几个控制点的矩阵。
当绘制曲线时,点下鼠标可以点击控制点的位置来调整任意曲线,而当前正在绘制的曲线会停止绘制。在当前点击位置寻找是否有控制点,若有,则开始改变控制点。
python
if self.display_ctrl_point==1:
tmp=find_curve([event.x,event.y],self.map_ctrl_point)
if tmp[0]!=-1:
self.is_curve_modifying=1
self.curve_modify_num=tmp
#print(tmp)
self.is_curve_painting=0
拖动鼠标时,实时改变位置并更新画布。
python
if self.is_curve_modifying==1:
self.primitives[self.curve_modify_num[0]].modify(self.curve_modify_num[1],[x,y])
松开鼠标时,结束变换。
python
if self.is_curve_modifying==1:
self.primitives[self.curve_modify_num[0]].modify(self.curve_modify_num[1],[event.x,event.y])
self.is_curve_modifying=0
效果如图:
Bezier:
B 样条:
参考文献
- 基于GIS的图形信息展示平台的研究与实现(西安工程大学·李锦)
- 基于Web的数据可视化系统设计及应用(北京邮电大学·刘铭宇)
- 基于FC-SAN的图形文件存储管理系统(电子科技大学·杨易)
- 幼儿图形及颜色识别软件的设计与实现(北京工业大学·连环)
- 基于J2EE架构的某学院图书管理信息系统设计与开发(电子科技大学·戴杰)
- 基于Web的数据可视化系统设计及应用(北京邮电大学·刘铭宇)
- Linux环境下基于Web的图档管理系统的开发(山东科技大学·刘治国)
- 建筑设计院图档管理系统的设计与实现(吉林大学·时淮龙)
- 建筑设计院图档管理系统的设计与实现(吉林大学·时淮龙)
- 基于B/S架构的图书管理系统的设计与实现(电子科技大学·郭汝奇)
- 中学python课程知识图谱构建及应用研究(华中师范大学·黄健)
- 基于B/S架构的图书管理系统的设计与实现(电子科技大学·郭汝奇)
- 文本综合处理平台的研究与实现(济南大学·王孟孟)
- 基于MapGIS的地图服务系统的设计与实现(中国地质大学·汪鹏)
- 基于深度学习的智能图像处理实验系统的设计与实现(哈尔滨工业大学·秦梓祺)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:毕设小屋 ,原文地址:https://bishedaima.com/yuanma/35931.html