在 Widget 上绘图





5.00/5 (1投票)
创建一个可以绘制的 PyQt Widget。

引言
本文基于我大约一年前写的一篇类似文章,当时我们在 C# 中实现了在面板上绘图,现在我们正在采用其主要结构并将其移植到 Qt 的 Python 版本 PyQt。原始文章可以在此处找到。
因此,我们将研究如何设计和实现可以在控件上绘图、绘制线条和擦除线条的功能,同时仍然允许用户自定义画笔的大小和颜色。
这种绘图方法更符合矢量图形,其中点是浮点值,而不是每个像素都具有颜色的栅格化位图图形。
旁注:我在 Maya 2012 这个程序中使用 PyQt,因为那是我的常规开发环境,此代码不使用任何 Maya 2012 Python 命令,因此在 IDLE 或任何其他 Python IDE 中使用时应该可以完美运行。
背景
本文设置为简单的入门级别;我们将涵盖创建类、创建控件和使用事件处理程序。您需要安装 PyQt 并使其与您正在使用的任何 Python IDE 一起工作。
使用代码
所以 Qt 最好的事情之一是 Maya 2012 默认附带的图形 UI 设计器。这个程序可以在 Maya.exe 所在的文件夹中找到,通常是 C:\Program Files\Autodesk\Maya2012\bin。
其他版本的 Qt 和 PyQt 也附带此功能,其位置通常在 C:\ 驱动器上的 Qt 根文件夹中。
UI 设计器
我设计的 UI 与一年前创建的 C# 版本完全相同,以展示代码可以多么容易地移植。
下面是设计器中的 UI,紧随其后是控件列表,包括它们的名称、类型、位置和大小,以便您可以精确地重新创建它。
控件类型 | 对象名称 | 职位 | 大小 |
QPushButton |
BrushErase_Button | 10,10 | 101,23 |
QPushButton |
ChangeColour_Button | 10,40 | 101,23 |
QPushButton |
Clear_Button | 10,400 | 101,23 |
QStackedWidget |
DrawingFrame | 120,10 | 651,411 |
QSpinBox |
Thickness_Spinner | 20,80 | 81,22 |
一旦这一切都设计好,我们就可以将其保存为 .UI 文件,我建议将其保存在脚本的常用位置,因为您需要将文件的地址硬编码进去,所以不要放得太远!!
好的,UI 全部完成后,我们需要跳到我们的 Python IDE,对我来说,就是 Maya 2012。
Python 端
UI 全部创建并保存后,我们需要首先让 Python 显示 UI。代码相当简单明了且通用。
from PyQt4 import QtGui,QtCore, uic uifile = 'G:/3D work/Python Scripts/Tuts/Paint on a panel PyQt/PaintOnAPanel.ui' form, base = uic.loadUiType(uifile) class CreateUI(base, form): def __init__(self): super(base,self).__init__() self.setupUi(self) def main(): global PyForm PyForm=CreateUI() PyForm.show() if __name__=="__main__": main()
运行此脚本将创建一个新的 PyQt 窗口,其中包含我们设计并保存的表单。简单部分完成。
为了支持代码,我们需要创建一些额外的类,这些类将保存数据,并处理保存的数据以移除和添加新点,以及保存关键信息,例如笔触的颜色和大小。
类
我们需要实现 4 个新类:**Colour3** 类,它将只保存 RGB 颜色值;**Point** 类,它将保存 X 和 Y 坐标;**Shape** 类,它保存有关该点的信息,例如位置、颜色、大小以及该点所连接的形状;以及 **Shapes** 类,它将保存所有形状,并提供创建、检索和删除它们的函数。
此外还有一个类,即 **Painter** 类,但这是一个控件类,所以我们稍后再讨论它。
Colour3 类
## My Own Colour Class, simple and light weight class Colour3: R = 0 G = 0 B = 0 #CONSTRUCTOR def __init__(self): self.R = 0 self.G = 0 self.B = 0 #CONSTRUCTOR - with the values to give it def __init__(self, nR, nG, nB): self.R = nR self.G = nG self.B = nB
Point 类
## My Own Point Class, simple and light weight class Point: #X Coordinate Value X = 0 #Y Coordinate Value Y = 0 #CONSTRUCTOR def __init__(self): self.X = 0 self.Y = 0 #CONSTRUCTOR - with the values to give it def __init__(self, nX, nY): self.X = nX self.Y = nY #So we can set both values at the same time def Set(self,nX, nY): self.X = nX self.Y = nY
这两个类都非常简单,所以我不会解释它们,它们只是用来保存少量数据。
Shape 类用于保存特定绘图点的数据,例如其位置、形状的宽度、颜色和形状编号,以便我们可以将所有相同的形状对象链接在一起。
## Shape class; holds data on the drawing point class Shape: Location = Point(0,0) Width = 0.0 Colour = Colour3(0,0,0) ShapeNumber = 0 #CONSTRUCTOR - with the values to give it def __init__(self, L, W, C, S): self.Location = L self.Width = W self.Colour = C self.ShapeNumber = SShapes 类用于保存所有绘图点信息,并提供快速易用的函数,以便绘图面板处理绘图点数据。
class Shapes: #Stores all the shapes __Shapes = [] def __init__(self): self.__Shapes = [] #Returns the number of shapes being stored. def NumberOfShapes(self): return len(self.__Shapes) #Add a shape to the database, recording its position, #width, colour and shape relation information def NewShape(self,L,W,C,S): Sh = Shape(L,W,C,S) self.__Shapes.append(Sh) #returns a shape of the requested data. def GetShape(self, Index): return self.__Shapes[Index] #Removes any point data within a certain threshold of a point. def RemoveShape(self, L, threshold): #do while so we can change the size of the list and it wont come back to bite me in the ass!! i = 0 while True: if(i==len(self.__Shapes)): break #Finds if a point is within a certain distance of the point to remove. if((abs(L.X - self.__Shapes[i].Location.X) < threshold) and (abs(L.Y - self.__Shapes[i].Location.Y) < threshold)): #removes all data for that number del self.__Shapes[i] #goes through the rest of the data and adds an extra #1 to defined them as a seprate shape and shuffles on the effect. for n in range(len(self.__Shapes)-i): self.__Shapes[n+i].ShapeNumber += 1 #Go back a step so we dont miss a point. i -= 1 i += 1
目前,让我们创建一个简单的虚拟控件,我们可以将其集成到主 UI 中,然后稍后回来充实它。
此代码将创建一个新类,该类将继承自 PyQt 控件类,因此我们可以在堆叠控件中使用它。
class Painter(QtGui.QWidget): Dum = 0 #we'll remove this later def __init__(self,parent): super(Painter, self).__init__()
主窗体
现在让我们开始设置 UI 交互的主要部分,这样我们就可以完成自定义控件的创建。
就像 C# 版本一样,我们需要一些变量来保存数据,例如;我们是在绘图还是擦除,当前选择的颜色以及当前的形状编号等等。我们可以在 CreateUI 类中,在构造函数之前快速声明这些变量。
class CreateUI(base, form): Brush = True DrawingShapes = Shapes() IsPainting = False IsEraseing = False CurrentColour = Colour3(0,0,0) CurrentWidth = 10 ShapeNum = 0 IsMouseing = False PaintPanel = 0
在我们将 PyQt 按钮与代码连接之前,我们需要定义它们将调用的函数。四个按钮意味着四个函数。
**SwitchBrush** 函数只是简单地将一个变量从 true 更改为 false,或从 false 更改为 true,这样我们就可以处于擦除模式或绘画模式。
**ChangeColour** 函数打开一个 Qt 调色板,允许用户选择他们想要的颜色,并保存以便我们以后使用。
**ChangeThickness** 获取微调框的新值,并将其设置为当前线条宽度。
**ClearSlate** 清除所有数据数组,以便我们可以重新开始。
def SwitchBrush(self): if(self.Brush == True): self.Brush = False else: self.Brush = True def ChangeColour(self): col = QtGui.QColorDialog.getColor() if col.isValid(): self.CurrentColour = Colour3(col.red(),col.green(),col.blue()) def ChangeThickness(self,num): self.CurrentWidth = num def ClearSlate(self): self.DrawingShapes = Shapes() self.PaintPanel.repaint()
要将 PyQt 按钮与函数连接起来,我们只需在一个函数中声明连接,并确保将该函数调用添加到类构造函数中。
def Establish_Connections(self): QtCore.QObject.connect(self.BrushErase_Button, QtCore.SIGNAL("clicked()"),self.SwitchBrush) QtCore.QObject.connect(self.ChangeColour_Button, QtCore.SIGNAL("clicked()"),self.ChangeColour) QtCore.QObject.connect(self.Clear_Button, QtCore.SIGNAL("clicked()"),self.ClearSlate) QtCore.QObject.connect(self.Thickness_Spinner, QtCore.SIGNAL("valueChanged(int)"),self.ChangeThickness)
在我们向类构造函数添加该函数调用的同时,我们也可以添加代码以将新的控件类插入到堆叠控件中。在这里,我们创建 Painter 控件类的一个实例,并保存对主类的引用,以便我们可以重新调用它,同时将此控件设置为堆叠控件上的当前控件。
#Constructor def __init__(self): super(base,self).__init__() self.setupUi(self) self.setObjectName('Rig Helper') self.PaintPanel = Painter(self) self.PaintPanel.close() self.DrawingFrame.insertWidget(0,self.PaintPanel) self.DrawingFrame.setCurrentWidget(self.PaintPanel) self.Establish_Connections()
主 UI 的代码已完成,我们只需定义如何使用存储在新控件中的所有这些日期,以允许用户在控件上绘图。
自定义控件类
在我们自己的自定义控件 Painter 中,我们需要首先定义一些将要使用的变量,以与在主 UI 类中定义变量相同的方式定义它们。
**ParentLink** 是指向主 UI 父类的引用链接,因此我们可以从中获取数据,例如颜色和大小,以及保存点数组。
**MouseLoc** 是鼠标所在位置的点。
**LastPos** 是鼠标上次所在位置的点。
值得注意的是,许多变量可以保存在 Painter 控件类中,使其自给自足,但我这样做是为了展示我们如何在不需要全局变量的情况下在控件之间进行交互。
在构造函数中,将 MouseLoc 和 LastPos 设置为默认值,并获取父级并将其存储在 ParentLink 下。
class Painter(QtGui.QWidget): ParentLink = 0 MouseLoc = Point(0,0) LastPos = Point(0,0) def __init__(self,parent): super(Painter, self).__init__() self.ParentLink = parent self.MouseLoc = Point(0,0) self.LastPos = Point(0,0)
控件中的所有回调都将通过事件处理程序完成,例如 mousePressEvent 事件,以便我们知道用户何时单击了控件,以及 paintEvent 事件,以便我们可以覆盖 paint 事件并使其绘制我们想要的内容。让我们从鼠标事件开始!
mousePressEvent 在鼠标按钮被按下时触发。这是一个简单的函数,它检查所需的模式,并将该模式设置为活动状态,并通过 self.ParentLink.ShapeNum += 1 行将其设置为新形状。
#Mouse down event def mousePressEvent(self, event): if(self.ParentLink.Brush == True): self.ParentLink.IsPainting = True self.ParentLink.ShapeNum += 1 self.LastPos = Point(0,0) else: self.ParentLink.IsEraseing = True
当鼠标在控件区域内移动时触发 mouseMoveEvent。该函数检查我们是否正在积极地绘图或擦除,如果我们在绘图,则在鼠标指针当前位置添加一个新点,或者如果我们在擦除,则在鼠标当前位置移除点。
#Mouse Move event def mouseMoveEvent(self, event): if(self.ParentLink.IsPainting == True): self.MouseLoc = Point(event.x(),event.y()) if((self.LastPos.X != self.MouseLoc.X) and (self.LastPos.Y != self.MouseLoc.Y)): self.LastPos = Point(event.x(),event.y()) self.ParentLink.DrawingShapes.NewShape(self.LastPos,self.ParentLink.CurrentWidth,self.ParentLink.CurrentColour,self.ParentLink.ShapeNum) self.repaint() if(self.ParentLink.IsEraseing == True): self.MouseLoc = Point(event.x(),event.y()) self.ParentLink.DrawingShapes.RemoveShape(self.MouseLoc,10) self.repaint()
mouseReleaseEvent 在鼠标按钮释放时触发,它只是简单地将所有活动模式设置为关闭,这样在我们不想绘图时就不会绘图。
#Mose Up Event def mouseReleaseEvent(self, event): if(self.ParentLink.IsPainting == True): self.ParentLink.IsPainting = False if(self.ParentLink.IsEraseing == True): self.ParentLink.IsEraseing = False
我们新控件类中覆盖的最后一个回调是 paintEvent,简单地说,它是重新绘制控件的函数。我们使用此函数在控件上绘制我们想要的内容。
为了配合此函数,我们有一个单独的函数名为 drawLines,因为我喜欢将绘图命令分离到单独的函数中,这样更容易操作。
drawLines 函数只是遍历点列表,如果它们的形状编号匹配,则在连接点之间绘制一条线,这意味着它们来自同一形状,使用保存的颜色和笔宽以及该特定点。
def paintEvent(self,event): painter = QtGui.QPainter() painter.begin(self) self.drawLines(event, painter) painter.end()
这意味着它们来自同一形状,使用该特定点保存的颜色和笔宽。
def drawLines(self, event, painter): painter.setRenderHint(QtGui.QPainter.Antialiasing); for i in range(self.ParentLink.DrawingShapes.NumberOfShapes()-1): T = self.ParentLink.DrawingShapes.GetShape(i) T1 = self.ParentLink.DrawingShapes.GetShape(i+1) if(T.ShapeNumber == T1.ShapeNumber): pen = QtGui.QPen(QtGui.QColor(T.Colour.R,T.Colour.G,T.Colour.B), T.Width/2, QtCore.Qt.SolidLine) painter.setPen(pen) painter.drawLine(T.Location.X,T.Location.Y,T1.Location.X,T1.Location.Y)
就是这样!!您现在拥有一个可以绘图的 UI。
关注点
我希望这能鼓励您探索 PyQt 还能做什么,以及将 C# 项目移植到 PyQt 以及类似地移植到 C++ Qt 有多么容易。很快就会有一个 C++ Qt 版本,它与这个版本相同。
历史
在此处保持您所做的任何更改或改进的实时更新。