在QGraphicsView中精确地以鼠标为锚缩放图片

在pyqt中以鼠标所在位置为锚点缩放图片-CSDN博客中的第一个示例中,通过简单设置:

self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)

使得QGraphicsView具有了以鼠标为锚进行缩放的功能。但是,其内部应当是利用了滚动条的移动来实现的,类似于下面这样:

// 连接滚动条信号,实时更新场景偏移值,在图片放大到超过视图区域时,

// 继续放大图片,鼠标位置对应的图片上的点不会在视图区移动
    connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
        sceneOffset.setX(-value);  // 注意负号
    });

    connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
        sceneOffset.setY(-value);  // 注意负号
    });
 

所以,当图片项缩小到视图范围以内,滚动条不再起作用时,就丧失了以鼠标为锚缩放图片的特性。同一篇文章的第二个示例用QLabel作为图片容器实现了严格的以鼠标为锚缩放图片的效果,但正如该文所指出的,涉及图片平移缩放等各种变换的程序,最佳的图片容器仍然是QGraphicsView,那么,如何在QGraphicsView中实现精确的以鼠标为锚缩放图片的效果呢?

这个问题实际上比以QLabel作为图片容器要复杂一些,因为以QLabel作为图片容器时,只有一个坐标系统,也就是以QLabel左上角为原点的坐标系统,容器中的所有图片的位置设置都相对于QLabel左上角计算。但是,在QGraphicsView框架中,却涉及三个坐标系统:
1、视图坐标系统(QGraphicsView的坐标系统):以QGraphicsView容器左上角为原点,并且从不变化。

2、场景坐标系统(QGraphicsScene的坐标系统):QGraphicsScene代表一个没有边界、没有大小的舞台(画布),它的原点是给QGraphicsItem定位的逻辑原点,在QGraphicsView的坐标系统中的值可能随着变换操作发生变化。在没有对场景做任何缩放和移动等变换的时候,这个逻辑原点等于QGraphicsView坐标系统中的原点坐标(0,0)。

3、图片项坐标系统(QGraphicsItem的坐标系统):在QGraphicsView框架中,图片等各种图形元素是在不同的QGraphicsItem中加载的,也就是说,一张图片就是一个QGraphicsItem(具体来说位图可以用QGraphicsPixmapItem来加载)。QGraphicsPixmapItem中的图片也有一个以自身左上角为原点的像素坐标系统,这个原点相对于场景坐标系统原点的坐标由QGraphicsItem::setPos()方法设置,可以通过QGraphicsItem::pos()方法取得。无论场景如何移动和变换,只要图形本身没有在场景上发生过位置变换,QGraphicsItem::pos()方法的返回值都是不变的。

如果要将QGraphicsItem的位置设置为视图坐标系统中的点(x,y),需要以下步骤:
1、定义想要放置Item的点target_view_point,其在视图坐标系统中的坐标为 (x,y)。
2、使用 QGraphicsView::mapToScene() 方法将这个视图坐标转换为场景坐标 scene_point。无论对视图做了什么变换(例如缩放和平移等),QGraphicsView框架都会自行处理,只要(x,y)不变,scene_point也不会发生变化。
3、使用 QGraphicsItem::setPos() 方法将QGraphicsItem左上角放置在转换后的场景坐标 scene_point。
示例代码如下:
 

from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsRectItem
from PyQt5.QtCore import Qt, QPointif __name__ == '__main__':app = QApplication([])scene = QGraphicsScene()view = QGraphicsView(scene)view.setWindowTitle("Place Item at View Coordinate")# 程序窗体将显示在屏幕(100, 100)的位置,这个点也就是视图坐标系的原点(0, 0)view.setGeometry(100, 100, 400, 300)# 您想要放置Item的视图坐标view_x = 50view_y = 75# 视图坐标系中的坐标(50, 75),屏幕坐标系中的坐标(150, 175)target_view_point = QPoint(view_x, view_y)# 将视图坐标转换为场景坐标,这个值由QGraphicsView框架计算得出,自己难以简单计算scene_point = view.mapToScene(target_view_point)# 创建一个QGraphicsRectItemrect = QGraphicsRectItem(0, 0, 50, 50)# 将QGraphicsRectItem左上角放置在转换后的场景坐标,也就放置在视图坐标系中的坐标(50, 75)处rect.setPos(scene_point)scene.addItem(rect)view.show()app.exec_()

根据对上述坐标系统之间的关系的理解,可以通过以下步骤实现QGraphicsView中以鼠标为锚点缩放图片的效果(仅考虑简单的平移缩放变换,图片项为pixmapItem ,缩放因子为zoomFactor,用C++的形式写伪代码):

1、获取鼠标在缩放前图片上的场景坐标:

1.1、将鼠标的视图坐标 mousePos 转换为场景坐标

mouseScenePos = mapToScene(mousePos.toPoint());

1.2、计算 mouseScenePos 相对于 pixmapItem 缩放前的场景坐标 pixmapItem->pos() 的偏移量:

offsetToMouse = mouseScenePos - pixmapItem->pos();

应当说明的是,这一步还有一个更健壮、更安全、适用能力更强、直接利用Qt Graphics View 框架内部的坐标转换机制的做法:

offsetToMouse = pixmapItem->mapFromScene(mouseScenePos);

 QGraphicsItem::mapFromScene(scenePoint): 这个函数的作用是将一个点从场景 (Scene) 的坐标系转换到该图形项 (item) 的局部坐标系。它考虑了该图形项自身以及其所有父项的完整变换(包括平移、缩放、旋转),返回值是这个点相对于该图形项左上角原点 (0,0) 的坐标。mapFromScene() 会正确处理图形项的平移、旋转和缩放。如果手动对 item 调用了 setRotation() 或 setScale(),或者它的父项有变换,那么简单的 scenePoint - item->pos() 就会得到错误的结果,而 mapFromScene() 仍然是正确的。因此,除非是确信极为简单的只有平移变换且不存在父项变换的情况,才推荐使用scenePoint - item->pos()手工计算的方式将场景坐标转换为图形项的局部坐标。

2、计算缩放后鼠标在图片上的场景坐标应该在的位置:

2.1、由于缩放是以 pixmapItem 的左上角为原点进行的(默认情况),那么 offsetToMouse 这个向量也应该被缩放 zoomFactor 倍。因此,缩放后鼠标在图片上的目标场景坐标应该是:

targetMouseScenePos = pixmapItem->pos() + offsetToMouse * zoomFactor;

注意,此时 pixmapItem->pos()的值与缩放前相比并未改变。

3、计算 pixmapItem 需要移动的偏移量:

为了使缩放后鼠标的目标场景坐标 targetMouseScenePos 与缩放前的鼠标场景坐标 mouseScenePos 在视图中的位置保持一致,pixmapItem 需要移动一定的距离。这个移动的距离按下式计算:

deltaMove = mouseScenePos - targetMouseScenePos;

4、更新 pixmapItem 的位置:

将 pixmapItem 的当前位置加上这个 deltaMove:

pixmapItem->setPos(pixmapItem->pos() + deltaMove);

根据上述步骤实现的在QGraphicsView中以鼠标为锚点缩放图片的完整Python示例如下:

from PyQt5.QtWidgets import (QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem,QPushButton, QVBoxLayout, QFileDialog, QWidget
)
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt, QPointF
import sysclass ImageViewer(QGraphicsView):def __init__(self, parent=None):super().__init__(parent)self.scene = QGraphicsScene(self)self.setScene(self.scene)# 保存原始图片和缩放信息self.original_pixmap = None  # 原始图片self.pixmap_item = None      # 场景中的图片项self.scale_factor = 1.0      # 缩放因子self.scene_offset = QPointF(0, 0)  # 场景在视图中的偏移量# 设置视图属性self.setDragMode(QGraphicsView.ScrollHandDrag)self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)# 固定视图大小self.setFixedSize(600, 600)def add_image(self, image_path):"""加载图片并显示"""self.scene.clear()self.original_pixmap = QPixmap(image_path)if self.original_pixmap.isNull():return# 初始化显示self.scale_factor = 1.0self.pixmap_item = self.scene.addPixmap(self.original_pixmap)self.pixmap_item.setTransformationMode(Qt.SmoothTransformation)# 计算初始缩放以适应视图view_size = self.size()scale = min(view_size.width() / self.original_pixmap.width(),view_size.height() / self.original_pixmap.height())# 应用初始缩放self.scale_factor = scalescaled_pixmap = self.original_pixmap.scaled(self.original_pixmap.size() * scale,Qt.KeepAspectRatio,Qt.SmoothTransformation)self.pixmap_item.setPixmap(scaled_pixmap)# 设置图片项可移动self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable) # 计算居中位置self.scene_offset = QPointF((view_size.width() - scaled_pixmap.width()) / 2,(view_size.height() - scaled_pixmap.height()) / 2)self.setSceneRect(0, 0, view_size.width(), view_size.height())self.pixmap_item.setPos(self.scene_offset)def wheelEvent(self, event):"""实现精确地以鼠标位置为锚点的缩放"""if not self.pixmap_item:returnself.pixmap_item.setCursor(Qt.ArrowCursor)# 获取鼠标相对于图片的位置mouse_pos = event.pos()mouseScenePos = self.mapToScene(mouse_pos)itemScenePos = self.pixmap_item.pos()# 下面注释掉的一行是更健壮适应更复杂的变换情况的做法# offsetToMouse = self.pixmap_item.mapFromScene(mouseScenePos)offsetToMouse = mouseScenePos - itemScenePos# 计算缩放因子if event.angleDelta().y() > 0:zoom_factor = 1.15else:zoom_factor = 1 / 1.15# 限制缩放倍数在0.1到10之间 tmp_scale_factor = self.scale_factor * zoom_factorif tmp_scale_factor > 10:zoom_factor = 10 / self.scale_factorelif tmp_scale_factor < 0.1:zoom_factor = 0.1 / self.scale_factor# 更新总缩放倍数self.scale_factor *= zoom_factor# 缩放图片scaled_pixmap = self.original_pixmap.scaled(self.original_pixmap.size() * self.scale_factor,Qt.KeepAspectRatio,Qt.SmoothTransformation)# 计算鼠标位置相对于图片的偏移量变化        targetMouseScenePos = itemScenePos + offsetToMouse * zoom_factordeltaMove = mouseScenePos - targetMouseScenePos# 更新图片和位置self.pixmap_item.setPixmap(scaled_pixmap)self.pixmap_item.setPos(self.pixmap_item.pos() + deltaMove)class MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("以鼠标为锚缩放图片示例")self.setGeometry(100, 100, 800, 600)# 创建主窗口布局central_widget = QWidget()self.setCentralWidget(central_widget)layout = QVBoxLayout(central_widget)# 创建 QGraphicsViewself.graphics_view = ImageViewer(self)layout.addWidget(self.graphics_view)# 创建按钮self.load_button = QPushButton("加载图片")self.load_button.clicked.connect(self.open_file_dialog)layout.addWidget(self.load_button)def open_file_dialog(self):"""打开文件对话框选择图片"""file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp)")if file_path:self.graphics_view.add_image(file_path)if __name__ == "__main__":app = QApplication(sys.argv)window = MainWindow()# 禁用最大化按钮window.setWindowFlags(window.windowFlags() & ~Qt.WindowMaximizeButtonHint)window.show()sys.exit(app.exec_())

运行效果如下

本文最后一个问题:图片缩放后,如果要从鼠标在视图中的坐标获取其在未经缩放的原始图片像素坐标中的坐标,该怎么做?其实很简单:

1、将鼠标坐标(视图鼠标事件中事件发生的位置,都是以视图坐标系确定的)转换为场景坐标系中的坐标,记为p1;
2、确定图片项的坐标系原点在场景中的坐标,记为p2;
3、p1-p2,即鼠标位置在图片项坐标系中的坐标,记为p3。图片项实际上就是经过缩放的图片;
4、将p3除以缩放倍数。

只考虑pixmapItem通过scaleFactor进行了均匀缩放的情况,具体的转换函数代码如下:

def viewPosToOriginalImgPos(self, viewPos: QPointF, scaleFactor: float) -> QPointF:scenePos = self.mapToScene(viewPos)itemPosInScene = self.pixmapItem.pos()posInItem = scenePos - itemPosInSceneoriginalImgX = posInItem.x() / scaleFactororiginalImgY = posInItem.y() / scaleFactorreturn QPointF(originalImgX, originalImgY)

如果x方向与y方向的缩放比例不同,可以通过

self.pixmapItem.transform().m11()   # 获取 x 方向的缩放,
self.pixmapItem.transform().m22()   # 获取 y 方向的缩放。

取得不同方向上的缩放倍数,从而将鼠标位置转换为正确的原始图片像素坐标系中的坐标。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78568.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

制造工厂如何借助电子看板实现高效生产管控

在当今高度竞争的制造业环境中&#xff0c;许多企业正面临着严峻的管理和生产挑战。首先&#xff0c;管理流程落后&#xff0c;大量工作仍依赖"人治"方式&#xff0c;高层管理者理论知识薄弱且不愿听取专业意见。其次&#xff0c;生产过程控制能力不足&#xff0c;导…

在 C# .NET 中驾驭 JSON:使用 Newtonsoft.Json 进行解析与 POST 请求实战

JSON (JavaScript Object Notation) 已经成为现代 Web 应用和服务之间数据交换的通用语言。无论你是开发后端 API、与第三方服务集成&#xff0c;还是处理配置文件&#xff0c;都绕不开 JSON 的解析与生成。在 C# .NET 世界里&#xff0c;处理 JSON 有多种选择&#xff0c;其中…

Debian10系统安装,磁盘分区和扩容

1、说明 过程记录信息有些不全&#xff0c;仅作为参考。如有其它疑问&#xff0c;欢迎留言。 2、ISO下载 地址&#xff1a;debian-10.13.0镜像地址 3、开始安装 3.1、选择图形界面 3.2、选择中文语言 3.3、选择中国区域 3.4、按照提示继续 3.5、选择一个网口 3.6、创建管…

1.10软考系统架构设计师:优秀架构设计师 - 练习题附答案及超详细解析

优秀架构设计师综合知识单选题 每道题均附有答案解析&#xff1a; 题目1 衡量优秀系统架构设计师的核心标准不包括以下哪项&#xff1f; A. 技术全面性与底层系统原理理解 B. 能够独立完成模块开发与调试 C. 与利益相关者的高效沟通与协调能力 D. 对业务需求和技术趋势的战略…

MPI Code for Ghost Data Exchange in 3D Domain Decomposition with Multi-GPUs

MPI Code for Ghost Data Exchange in 3D Domain Decomposition with Multi-GPUs Here’s a comprehensive MPI code that demonstrates ghost data exchange for a 3D domain decomposition across multiple GPUs. This implementation assumes you’re using CUDA-aware MPI…

计算机考研精炼 计网

第 19 章 计算机网络体系结构 19.1 基本概念 19.1.1 计算机网络概述 1.计算机网络的定义、组成与功能 计算机网络是一个将分散的、具有独立功能的计算机系统&#xff0c;通过通信设备与线路连接起来&#xff0c;由功能完善的软件实现资源共享和信息传递的系统。 …

KUKA机器人自动备份设置

在机器人的使用过程中&#xff0c;对机器人做备份不仅能方便查看机器人的项目配置与程序&#xff0c;还能防止机器人项目和程序丢失时进行及时的还原&#xff0c;因此对机器人做备份是很有必要的。 对于KUKA机器人来说&#xff0c;做备份可以通过U盘来操作。也可以在示教器上设…

【wpf】 WPF中实现动态加载图片浏览器(边滚动边加载)

WPF中实现动态加载图片浏览器&#xff08;边滚动边加载&#xff09; 在做图片浏览器程序时&#xff0c;遇到图片数量巨大的情况&#xff08;如几百张、上千张&#xff09;&#xff0c;一次性加载所有图片会导致界面卡顿甚至程序崩溃。 本文介绍一种 WPF Prism 实现动态分页加…

Kubernetes》》k8s》》Taint 污点、Toleration容忍度

污点 》》 节点上 容忍度 》》 Pod上 在K8S中&#xff0c;如果Pod能容忍某个节点上的污点&#xff0c;那么Pod就可以调度到该节点。如果不能容忍&#xff0c;那就无法调度到该节点。 污点和容忍度的概念 》》污点等级——>node 》》容忍度 —>pod Equal——>一种是等…

SEO长尾关键词优化核心策略

内容概要 在搜索引擎优化领域&#xff0c;长尾关键词因其精准的流量捕获能力与较低的竞争强度&#xff0c;已成为提升网站自然流量的核心突破口。本文围绕长尾关键词优化的全链路逻辑&#xff0c;系统拆解从需求洞察到落地执行的五大策略模块&#xff0c;涵盖用户搜索意图解析…

AWS中国区ICP备案全攻略:流程、注意事项与最佳实践

导语 在中国大陆地区开展互联网业务时,所有通过域名提供服务的网站和应用必须完成ICP备案(互联网内容提供商备案)。对于选择使用AWS中国区(北京/宁夏区域)资源的用户,备案流程因云服务商的特殊运营模式而有所不同。本文将详细解析AWS中国区备案的核心规则、操作步骤及避坑…

计算机视觉——通过 OWL-ViT 实现开放词汇对象检测

介绍 传统的对象检测模型大多是封闭词汇类型&#xff0c;只能识别有限的固定类别。增加新的类别需要大量的注释数据。然而&#xff0c;现实世界中的物体类别几乎无穷无尽&#xff0c;这就需要能够检测未知类别的开放式词汇类型。对比学习&#xff08;Contrastive Learning&…

大语言模型的“模型量化”详解 - 04:KTransformers MoE推理优化技术

基本介绍 随着大语言模型&#xff08;LLM&#xff09;的规模不断扩大&#xff0c;模型的推理效率和计算资源的需求也在迅速增加。DeepSeek-V2作为当前热门的LLM之一&#xff0c;通过创新的架构设计与优化策略&#xff0c;在资源受限环境下实现了高效推理。 本文将详细介绍Dee…

排序算法详解笔记

评价维度 运行效率就地性稳定性 自适应性&#xff1a;自适应排序能够利用输入数据已有的顺序信息来减少计算量&#xff0c;达到更优的时间效率。自适应排序算法的最佳时间复杂度通常优于平均时间复杂度。 是否基于比较&#xff1a;基于比较的排序依赖比较运算符&#xff08;…

【“星瑞” O6 评测】 — llm CPU部署对比高通骁龙CPU

前言 随着大模型应用场景的不断拓展&#xff0c;arm cpu 凭借其独特优势在大模型推理领域的重要性日益凸显。它在性能、功耗、架构适配等多方面发挥关键作用&#xff0c;推动大模型在不同场景落地 1. CPU对比 星睿 O6 CPU 采用 Armv9 架构&#xff0c;集成了 Armv9 CPU 核心…

Ocelot的应用案例

搭建3个项目&#xff0c;分别是OcelotDemo、ServerApi1和ServerApi2这3个项目。访问都是通过OcelotDemo进行轮训转发。 代码案例链接&#xff1a;https://download.csdn.net/download/ly1h1/90715035 1.架构图 2.解决方案结构 3.步骤一&#xff0c;添加Nuget包 4.步骤二&…

DeepSeek+Dify之五工作流引用API案例

DeepSeekDify之四Agent引用知识库案例 文章目录 背景整体流程测试数据用到的节点开始HTTP请求LLM参数提取器代码执行结束 实现步骤1、新建工作流2、开始节点3、Http请求节点4、LLM节点&#xff08;大模型检索&#xff09;5、参数提取器节点&#xff08;提取大模型检索后数据&am…

《从分遗产说起:JS 原型与继承详解》

“天天开心就好” 先来讲讲概念&#xff1a; 原型&#xff08;Prototype&#xff09; 什么是原型&#xff1f; 原型是 JavaScript 中实现对象间共享属性和方法的机制。每个 JavaScript 对象&#xff08;除了 null&#xff09;都有一个内部链接指向另一个对象&#xff0c;这…

立马耀:通过阿里云 Serverless Spark 和 Milvus 构建高效向量检索系统,驱动个性化推荐业务

作者&#xff1a;厦门立马耀网络科技有限公司大数据开发工程师 陈宏毅 背景介绍 行业 蝉选是蝉妈妈出品的达人选品服务平台。蝉选秉持“陪伴达人赚到钱”的品牌使命&#xff0c;致力于洞悉达人变现需求和痛点&#xff0c;提供达人选高佣、稳变现、速响应的选品服务。 业务特…

Android显示学习笔记本

根据博客 Android-View 绘制原理(01)-JAVA层分析_android view draw原理分析-CSDN博客 提出了我的疑问 Canvas RenderNode updateDisplayListDirty 这些东西的关系 您的理解在基本方向上是对的&#xff0c;但让我详细解释一下 Android 中 updateDisplayListDirty、指令集合、…