python QT graphicsView控件实现图片的缩放与移动
- 1、效果图
- 2、界面搭建
- 3、实现方法
- 3.1、构建处理图元的类
- 3.1、绘制图像
- 3.2、拖拽方法实现
- 3.3、缩放方法实现
- 4、调用方法
1、效果图
选择图片后可在graphicsView窗口中显示选择的图片,可以用鼠标拖拽图片。当鼠标停在图片上时滚动滑轮,以鼠标位置为中心缩放;当鼠标不在图片上时滚动滑轮,以图片自身中心进行缩放。
2、界面搭建
利用Qt designer 添加graphicsView控件。整个界面由两个垂直布局的groupBox组成,上面的groupBox中仅有一个graphicsView控件(即下图中红箭头所指的控件),下面的groupBox仅包含一个按钮,用以选择图片。
3、实现方法
3.1、构建处理图元的类
该类继承于QWidget。构造方法中除了图形界面初始化外,还将图形视图的内边距和边界去除、改变图形视图的对齐方式、设置场景大小和图形视图大小一致,同时接管图形场景的mousePressEvent 、mouseMoveEvent 、wheelEvent 方法(用来实现鼠标点击与滑轮滚动的自定义事件),具体代码如下:
class IMG_WIN(QWidget):
def __init__(self,graphicsView):
super().__init__()
self.graphicsView=graphicsView
self.graphicsView.setStyleSheet("padding: 0px; border: 0px;") # 内边距和边界去除
self.scene = QtWidgets.QGraphicsScene(self)
self.graphicsView.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) # 改变对齐方式
self.graphicsView.setSceneRect(0, 0, self.graphicsView.viewport().width(),
self.graphicsView.height()) # 设置图形场景大小和图形视图大小一致
self.graphicsView.setScene(self.scene)
self.scene.mousePressEvent = self.scene_MousePressEvent # 接管图形场景的鼠标点击事件
# self.scene.mouseReleaseEvent = self.scene_mouseReleaseEvent
self.scene.mouseMoveEvent = self.scene_mouseMoveEvent # 接管图形场景的鼠标移动事件
self.scene.wheelEvent = self.scene_wheelEvent # 接管图形场景的滑轮事件
self.ratio = 1 # 缩放初始比例
self.zoom_step = 0.1 # 缩放步长
self.zoom_max = 2 # 缩放最大值
self.zoom_min = 0.2 # 缩放最小值
self.pixmapItem=None
3.1、绘制图像
def addScenes(self,img): # 绘制图形
self.org = img
if self.pixmapItem != None:
originX = self.pixmapItem.x()
originY = self.pixmapItem.y()
else:
originX, originY = 0, 0 # 坐标基点
self.scene.clear() # 清除当前图元
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # opencv读取的bgr格式图片转换成rgb格式
self.pixmap = QtGui.QPixmap(
QtGui.QImage(img[:], img.shape[1], img.shape[0], img.shape[1] * 3,
QtGui.QImage.Format_RGB888)) # 转化为qlbel格式
self.pixmapItem = self.scene.addPixmap(self.pixmap)
self.pixmapItem.setScale(self.ratio) # 缩放
self.pixmapItem.setPos(originX, originY)
在IMG_WIN类中添加addScenes方法,并添加img参数。调用该方法即可在窗口中显示图片,传入的img参数是用opencv读入的图片对象。
3.2、拖拽方法实现
def scene_MousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton: # 左键按下
# print("鼠标左键单击") # 响应测试语句
# print(event.scenePos())
self.preMousePosition = event.scenePos() # 获取鼠标当前位置
# if event.button() == QtCore.Qt.RightButton: # 右键按下
# print("鼠标右键单击") # 响应测试语句
def scene_mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
# print("左键移动") # 响应测试语句
self.MouseMove = event.scenePos() - self.preMousePosition # 鼠标当前位置-先前位置=单次偏移量
self.preMousePosition = event.scenePos() # 更新当前鼠标在窗口上的位置,下次移动用
self.pixmapItem.setPos(self.pixmapItem.pos() + self.MouseMove) # 更新图元位置
拖拽移动的关键是获取鼠标按住并拖动的位移量。scene_MousePressEvent在鼠标点击时触发,通过event.scenePos()获取点击的位置;scene_mouseMoveEvent在鼠标按下并移动过程中触发,当前位置与之前位置作差即可得到鼠标的位移量。之后通过setPos()方法更新图元的位置,就实现了拖拽移动的功能。注意scene_MousePressEvent和scene_mouseMoveEvent中判断按键状态一个是button,一个是buttons,两者是不一样的,大家可自行查阅一下。
3.3、缩放方法实现
# 定义滚轮方法。当鼠标在图元范围之外,以图元中心为缩放原点;当鼠标在图元之中,以鼠标悬停位置为缩放中心
def scene_wheelEvent(self, event):
angle = event.delta() / 8 # 返回QPoint对象,为滚轮转过的数值,单位为1/8度
if angle > 0:
# print("滚轮上滚")
self.ratio += self.zoom_step # 缩放比例自加
if self.ratio > self.zoom_max:
self.ratio = self.zoom_max
else:
w = self.pixmap.size().width() * (self.ratio - self.zoom_step)
h = self.pixmap.size().height() * (self.ratio - self.zoom_step)
x1 = self.pixmapItem.pos().x() # 图元左位置
x2 = self.pixmapItem.pos().x() + w # 图元右位置
y1 = self.pixmapItem.pos().y() # 图元上位置
y2 = self.pixmapItem.pos().y() + h # 图元下位置
if event.scenePos().x() > x1 and event.scenePos().x() < x2 \
and event.scenePos().y() > y1 and event.scenePos().y() < y2: # 判断鼠标悬停位置是否在图元中
# print('在内部')
self.pixmapItem.setScale(self.ratio) # 缩放
a1 = event.scenePos() - self.pixmapItem.pos() # 鼠标与图元左上角的差值
a2=self.ratio/(self.ratio- self.zoom_step)-1 # 对应比例
delta = a1 * a2
self.pixmapItem.setPos(self.pixmapItem.pos() - delta)
# ----------------------------分维度计算偏移量-----------------------------
# delta_x = a1.x()*a2
# delta_y = a1.y()*a2
# self.pixmapItem.setPos(self.pixmapItem.pos().x() - delta_x,
# self.pixmapItem.pos().y() - delta_y) # 图元偏移
# -------------------------------------------------------------------------
else:
# print('在外部') # 以图元中心缩放
self.pixmapItem.setScale(self.ratio) # 缩放
delta_x = (self.pixmap.size().width() * self.zoom_step) / 2 # 图元偏移量
delta_y = (self.pixmap.size().height() * self.zoom_step) / 2
self.pixmapItem.setPos(self.pixmapItem.pos().x() - delta_x,
self.pixmapItem.pos().y() - delta_y) # 图元偏移
else:
# print("滚轮下滚")
self.ratio -= self.zoom_step
if self.ratio < 0.2:
self.ratio = 0.2
else:
w = self.pixmap.size().width() * (self.ratio + self.zoom_step)
h = self.pixmap.size().height() * (self.ratio + self.zoom_step)
x1 = self.pixmapItem.pos().x()
x2 = self.pixmapItem.pos().x() + w
y1 = self.pixmapItem.pos().y()
y2 = self.pixmapItem.pos().y() + h
# print(x1, x2, y1, y2)
if event.scenePos().x() > x1 and event.scenePos().x() < x2 \
and event.scenePos().y() > y1 and event.scenePos().y() < y2:
# print('在内部')
self.pixmapItem.setScale(self.ratio) # 缩放
a1 = event.scenePos() - self.pixmapItem.pos() # 鼠标与图元左上角的差值
a2=self.ratio/(self.ratio+ self.zoom_step)-1 # 对应比例
delta = a1 * a2
self.pixmapItem.setPos(self.pixmapItem.pos() - delta)
# ----------------------------分维度计算偏移量-----------------------------
# delta_x = a1.x()*a2
# delta_y = a1.y()*a2
# self.pixmapItem.setPos(self.pixmapItem.pos().x() - delta_x,
# self.pixmapItem.pos().y() - delta_y) # 图元偏移
# -------------------------------------------------------------------------
else:
# print('在外部')
self.pixmapItem.setScale(self.ratio)
delta_x = (self.pixmap.size().width() * self.zoom_step) / 2
delta_y = (self.pixmap.size().height() * self.zoom_step) / 2
self.pixmapItem.setPos(self.pixmapItem.pos().x() + delta_x, self.pixmapItem.pos().y() + delta_y)
看似很长,其实很多都是类似的,不要慌。
首先说明一下图元是通过setScale()方法实现缩放的,但默认是以左上角为原点进行缩放,我没有找到可以调整缩放中心的方法。所以要实现以鼠标为中心缩放,是先缩放再平移。若有更简便的方法,还请大佬不吝赐教。
首先通过event.delta()获取滚轮的拨动方向,这个值的大小不必在意,主要是正负号判断向上还是向下滚动。我们以大于0(放大)为例说明一下实现的步骤。首先是限幅判断,如果放大比例超过上限则不作处理。(x1,y1)为缩放前图元左上角坐标,(x2,y2)为缩放前图元右下角坐标。之后用event.scenePos()获取鼠标当前的位置,与(x1,y1)和(x2,y2)比较就能确定鼠标是不是在图元当中,若是则以鼠标为中心缩放,若鼠标在图元之外则以图元中心进行缩放。
第一种情况是以鼠标位置为中心缩放:
假设现在由红框放大至蓝框,放大比例为k。鼠标放置在红点位置,那么放大过后红点对应到蓝点,若想鼠标停放的位置为蓝点,则需要将蓝框位移(delta_x,delta_y),这样就达到了以鼠标为中心缩放的效果。
我们以x轴偏移量计算为例。上图中有红线放大到蓝线,比例依然为k。x0,x1为线段端点。计算出任意点x对应的点x2,x2-x即为x轴的偏移量delta_x。
同理可以算出
a1 = event.scenePos() - self.pixmapItem.pos() # 鼠标与图元左上角的差值
a2=self.ratio/(self.ratio- self.zoom_step)-1 # 对应比例
delta = a1 * a2
self.pixmapItem.setPos(self.pixmapItem.pos() - delta)
即代码中。最后通过setPos()方法将偏移量加上去就OK了。
第二种情况是以图片自身中心缩放
当鼠标不在图元上时以鼠标为中心缩放就没有意义了,这时切换到以图元自身中心缩放。这时套用上面的公式也是可以的,令即可。但其实如果按图元中心缩放,每次的缩放比例又一致的话,那么每次的位移量其实都是放大差值的一半,就不用上面麻烦的算了。
delta_x = (self.pixmap.size().width() * self.zoom_step) / 2 # 图元偏移量
delta_y = (self.pixmap.size().height() * self.zoom_step) / 2
self.pixmapItem.setPos(self.pixmapItem.pos().x() - delta_x,
self.pixmapItem.pos().y() - delta_y) # 图元偏移
4、调用方法
class GUI(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load('img_view.ui')
self.graphic=IMG_WIN(self.ui.graphicsView) # 实例化IMG_WIN类
self.ui.pushButton.clicked.connect(self.select_img)
def select_img(self):
filePath, _ = QFileDialog.getOpenFileName(
self.ui, # 父窗口对象
"选择你要上传的图片", # 标题
r"E:\picture\test", # 起始目录
"图片类型 (*.png *.jpg *.bmp)" # 选择类型过滤项,过滤内容在括号中
)
if filePath == '':
return
else:
img = cv2.imread(filePath)
self.graphic.addScenes(img)
只需在主界面中将IMG_WIN类实例化,并传入预先定义好的graphicsView对象即可。需要显示图片则调用IMG_WIN类中的addScenes方法,传入的为opencv读取的图片,其他格式则需要另外的修改才能正常显示。
本人也是初学,若有不对的地方欢迎大佬指正!,完整代码已上传至GitHub,https://github.com/risemeup/pyside2View。如果能帮到素未谋面的你,点个星星,交个朋友。