在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.pswp.cn/pingmian/78568.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/78568.shtml
英文地址,請注明出處:http://en.pswp.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、指令集合、…