python QT 图片缩放,移动_qtdesigner中怎么让图片随着控件缩放而变化-程序员宅基地

技术标签: python  数据可视化  

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。
x − x 0 x 2 − x 0 = x 1 − x 0 k ( x 1 − x 0 ) \frac {x-x_0} {x_2-x_0}=\frac {x_1-x_0} {k(x_1-x_0)} x2x0xx0=k(x1x0)x1x0
x 2 = k ( x − x 0 ) + x 0 x_2=k(x-x_0)+x_0 x2=k(xx0)+x0
d e l t a x = x 2 − x = ( k − 1 ) ( x − x 0 ) deltax=x_2-x=(k-1)(x-x_0) deltax=x2x=(k1)(xx0)
同理可以算出 d e l t a y = ( k − 1 ) ( y − y 0 ) deltay=(k-1)(y-y_0) deltay=(k1)(yy0)

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)

即代码中 a 1 = x − x 0 , a 2 = k − 1 a1=x-x_0,a2=k-1 a1=xx0,a2=k1。最后通过setPos()方法将偏移量加上去就OK了。

第二种情况是以图片自身中心缩放
当鼠标不在图元上时以鼠标为中心缩放就没有意义了,这时切换到以图元自身中心缩放。这时套用上面的公式也是可以的,令 x = x 0 + x 1 2 x=\frac{x_0+x_1}{2} x=2x0+x1即可。但其实如果按图元中心缩放,每次的缩放比例又一致的话,那么每次的位移量其实都是放大差值的一半,就不用上面麻烦的算了。

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。如果能帮到素未谋面的你,点个星星,交个朋友。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44821251/article/details/106290132

智能推荐

前端开发之vue-grid-layout的使用和实例-程序员宅基地

文章浏览阅读1.1w次,点赞7次,收藏34次。vue-grid-layout的使用、实例、遇到的问题和解决方案_vue-grid-layout

Power Apps-上传附件控件_powerapps点击按钮上传附件-程序员宅基地

文章浏览阅读218次。然后连接一个数据源,就会在下面自动产生一个添加附件的组件。把这个控件复制粘贴到页面里,就可以单独使用来上传了。插入一个“编辑”窗体。_powerapps点击按钮上传附件

C++ 面向对象(Object-Oriented)的特征 & 构造函数& 析构函数_"object(cnofd[\"ofdrender\"])十条"-程序员宅基地

文章浏览阅读264次。(1) Abstraction (抽象)(2) Polymorphism (多态)(3) Inheritance (继承)(4) Encapsulation (封装)_"object(cnofd[\"ofdrender\"])十条"

修改node_modules源码,并保存,使用patch-package打补丁,git提交代码后,所有人可以用到修改后的_修改 node_modules-程序员宅基地

文章浏览阅读133次。删除node_modules,重新npm install看是否成功。在 package.json 文件中的 scripts 中加入。修改你的第三方库的bug等。然后目录会多出一个目录文件。_修改 node_modules

【】kali--password:su的 Authentication failure问题,&sudo passwd root输入密码时Sorry, try again._password: su: authentication failure-程序员宅基地

文章浏览阅读883次。【代码】【】kali--password:su的 Authentication failure问题,&sudo passwd root输入密码时Sorry, try again._password: su: authentication failure

整理5个优秀的微信小程序开源项目_微信小程序开源模板-程序员宅基地

文章浏览阅读1w次,点赞13次,收藏97次。整理5个优秀的微信小程序开源项目。收集了微信小程序开发过程中会使用到的资料、问题以及第三方组件库。_微信小程序开源模板

随便推点

Centos7最简搭建NFS服务器_centos7 搭建nfs server-程序员宅基地

文章浏览阅读128次。Centos7最简搭建NFS服务器_centos7 搭建nfs server

Springboot整合Mybatis-Plus使用总结(mybatis 坑补充)_mybaitis-plus ruledataobjectattributemapper' and '-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏3次。前言mybatis在持久层框架中还是比较火的,一般项目都是基于ssm。虽然mybatis可以直接在xml中通过SQL语句操作数据库,很是灵活。但正其操作都要通过SQL语句进行,就必须写大量的xml文件,很是麻烦。mybatis-plus就很好的解决了这个问题。..._mybaitis-plus ruledataobjectattributemapper' and 'com.picc.rule.management.d

EECE 1080C / Programming for ECESummer 2022 Laboratory 4: Global Functions Practice_eece1080c-程序员宅基地

文章浏览阅读325次。EECE 1080C / Programming for ECESummer 2022Laboratory 4: Global Functions PracticePlagiarism will not be tolerated:Topics covered:function creation and call statements (emphasis on global functions)Objective:To practice program development b_eece1080c

洛谷p4777 【模板】扩展中国剩余定理-程序员宅基地

文章浏览阅读53次。被同机房早就1年前就学过的东西我现在才学,wtcl。设要求的数为\(x\)。设当前处理到第\(k\)个同余式,设\(M = LCM ^ {k - 1} _ {i - 1}\) ,前\(k - 1\)个的通解就是\(x + i * M\)。那么其实第\(k\)个来说,其实就是求一个\(y\)使得\(x + y * M ≡ a_k(mod b_k)\)转化一下就是\(y * M ...

android 退出应用没有走ondestory方法,[Android基础论]为何Activity退出之后,系统没有调用onDestroy方法?...-程序员宅基地

文章浏览阅读1.3k次。首先,问题是如何出现的?晚上复查代码,发现一个activity没有调用自己的ondestroy方法我表示非常的费解,于是我检查了下代码。发现再finish代码之后接了如下代码finish();System.exit(0);//这就是罪魁祸首为什么这样写会出现问题System.exit(0);////看一下函数的原型public static void exit (int code)//Added ..._android 手动杀死app,activity不执行ondestroy

SylixOS快问快答_select函数 导致堆栈溢出 sylixos-程序员宅基地

文章浏览阅读894次。Q: SylixOS 版权是什么形式, 是否分为<开发版税>和<运行时版税>.A: SylixOS 是开源并免费的操作系统, 支持 BSD/GPL 协议(GPL 版本暂未确定). 没有任何的运行时版税. 您可以用她来做任何 您喜欢做的项目. 也可以修改 SylixOS 的源代码, 不需要支付任何费用. 当然笔者希望您可以将使用 SylixOS 开发的项目 (不需要开源)或对 SylixOS 源码的修改及时告知笔者.需要指出: SylixOS 本身仅是笔者用来提升自己水平而开发的_select函数 导致堆栈溢出 sylixos

推荐文章

热门文章

相关标签