在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 方向的縮放。
取得不同方向上的縮放倍數,從而將鼠標位置轉換為正確的原始圖片像素坐標系中的坐標。