在編寫涉及到圖片縮放的pyqt程序時,如果以鼠標為錨點縮放圖片,圖片上處于鼠標所在位置的點(通常也是用戶關注的圖片上的點)不會移動,更不會消失在圖片顯示區域之外,可以提高用戶體驗,是一個值得實現的效果。
實現以鼠標所在位置為錨點進行圖片縮放的效果的最簡單的方法是以QGraphicsView作為圖片的容器,只需要在初始化時設置:
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
即可實現以鼠標為錨點縮放圖片的效果。下面是一個簡單示例:
from PyQt5.QtWidgets import (QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QPushButton, QVBoxLayout, QFileDialog, QWidget
)
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt
import sysclass ImageViewer(QGraphicsView):def __init__(self, parent=None):super().__init__(parent)self.scene = QGraphicsScene(self)self.setScene(self.scene)self.scale_factor = 1.0 # 縮放因子self.setDragMode(QGraphicsView.ScrollHandDrag) # 設置拖動模式self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) # 設置縮放錨點為鼠標位置def add_image(self, image_path):"""向 QGraphicsView 中添加一張圖片"""pixmap = QPixmap(image_path)if pixmap.isNull():return# 創建 QGraphicsPixmapItem 并添加到場景中pixmap_item = self.scene.addPixmap(pixmap)pixmap_item.setTransformationMode(Qt.SmoothTransformation)# 其他圖片:可移動、可縮放pixmap_item.setFlags(pixmap_item.flags() | pixmap_item.ItemIsMovable) # 添加可移動標志def wheelEvent(self, event):"""實現以鼠標位置為錨點的縮放"""# 獲取鼠標在視圖中的位置# mouse_pos = event.pos()# 將鼠標位置轉換為場景坐標# scene_pos = self.mapToScene(mouse_pos)# 根據滾輪方向調整縮放因子if event.angleDelta().y() > 0:scale_change = 1.15 # 放大else:scale_change = 1 / 1.15 # 縮小# 執行縮放,水平方向與垂直方向等比例縮放self.scale(scale_change, scale_change)class MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("Image Viewer with Multiple Images")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、創建了一個類ImageViewer繼承QGraphicsView,在__init__方法中設置以鼠標所在位置為錨點縮放:
# 設置縮放錨點為鼠標位置
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) ?
2、通過QGraphicsPixmapItem加載一張圖片,然后將QGraphicsPixmapItem加入場景:
# 創建并設置場景
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
pixmap = QPixmap(image_path)
if pixmap.isNull():
? ? ? return
# 創建 QGraphicsPixmapItem 加載圖片,并將QGraphicsPixmapItem添加到場景中
pixmap_item = self.scene.addPixmap(pixmap)
3、重寫wheelEvent事件實現圖片縮放:
def wheelEvent(self, event):
? ? ? ? """實現以鼠標位置為錨點的縮放"""
? ? ? ? # 根據滾輪方向調整縮放因子
? ? ? ? if event.angleDelta().y() > 0:
? ? ? ? ? ? scale_change = 1.15 ?# 放大
? ? ? ? else:
? ? ? ? ? ? scale_change = 1 / 1.15 ?# 縮小
? ? ? ? # 執行縮放,水平方向與垂直方向等比例縮放
? ? ? ? self.scale(scale_change, scale_change)
可以看到,通過以上三個步驟,基本上不用添加與圖片位置相關的代碼,QGraphicsView就實現了以鼠標所在位置為錨點縮放圖片的效果。
但是,上面這種QGraphicsView的實現雖然簡單,其實不夠精確。將圖片不斷縮小,會發現縮小到某個程度后圖片會脫離鼠標所在位置。那么如何實現精確的以鼠標所在位置為錨點縮放圖片的效果呢?
先看以下圖示:
首先有一個顯示圖片的容器,例如QLabel,給這個容器一個固定的尺寸,它有一個以左上角為(0,0)的坐標系。
其次有一張圖片,為美觀起見,將這張圖片(實際上是經過縮放后的圖片)水平垂直居中顯示在容器中,圖片自身擁有一個以圖片的左上角為(0,0)的像素坐標系。像素坐標系與容器坐標系的原點存在偏差(img_offset),而圖片上所有像素在圖片像素坐標系中的坐標與在容器坐標系中的坐標的偏差與兩個坐標系原點的偏差是相等的。經過縮放后,鼠標在圖片上的位置會發生偏移delta_offset,只需將img_offset調整delta_offset,那么,縮放后的鼠標位置就調整回了縮放前的位置,形成了以鼠標為錨點的縮放效果。應當注意的是,容器響應鼠標事件時,鼠標位置的值是以容器坐標系的數據表示的,而前述調整img_offset的方法是基于圖片像素坐標系的,因此要將容器坐標系表示的鼠標位置轉換為圖片像素坐標系,方法如下(以下代碼中img_offset采用了scaled_pixmap_offset的變量名):
mouse_pos_on_scaled_pixmap = event.pos() - self.scaled_pixmap_offset
也就是說,需要在wheelEvent事件中添加一些坐標變換的代碼,具體如下:
def wheelEvent(self, event: QWheelEvent):"""重寫滾輪事件,實現縮放"""mouse_pos_on_scaled_pixmap = event.pos() - self.scaled_pixmap_offset# 根據滾輪方向調整縮放因子if event.angleDelta().y() > 0:zoom_factor = 1.15 # 放大else:zoom_factor = 1 / 1.15 # 縮小# 計算累計縮放比例self.scale_factor *= zoom_factor# 縮放圖片self.scaled_pixmap = self.original_pixmap.scaled(self.size() * self.scale_factor,Qt.KeepAspectRatio,Qt.SmoothTransformation)# 計算鼠標在圖片縮放前后的偏移量delta_offset = mouse_pos_on_scaled_pixmap * (zoom_factor - 1)# 調整圖像顯示位置對容器左上角的偏移量self.scaled_pixmap_offset -= delta_offset# 更新圖像顯示self.update()
完整示例如下:
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QFileDialog, QPushButton
from PyQt5.QtGui import QPixmap, QImage, QWheelEvent, QPainter
from PyQt5.QtCore import Qt, QPointFclass ImageLabel(QLabel):def __init__(self, parent=None):super().__init__(parent)self.setAlignment(Qt.AlignCenter) # 圖像居中顯示self.scale_factor = 1.0 # 縮放因子self.original_pixmap = None # 原始圖像self.scaled_pixmap = None # 縮放后的圖像self.setFixedSize(600, 600)self.scaled_pixmap_offset = QPointF(0, 0)def set_image(self, image_path):"""加載圖像并顯示"""self.original_pixmap = QPixmap(image_path)if self.original_pixmap.isNull():returnif self.scaled_pixmap is None:self.scaled_pixmap = self.original_pixmap.scaled(self.size(),Qt.KeepAspectRatio,Qt.SmoothTransformation)self.scaled_pixmap_offset_x = (self.size().width() - self.scaled_pixmap.size().width()) / 2self.scaled_pixmap_offset_y = (self.size().height() - self.scaled_pixmap.size().height()) / 2self.scaled_pixmap_offset = QPointF(self.scaled_pixmap_offset_x, self.scaled_pixmap_offset_y)self.update()def paintEvent(self, event):painter = QPainter(self)if self.scaled_pixmap is not None:painter.drawPixmap(self.scaled_pixmap_offset, self.scaled_pixmap)def wheelEvent(self, event: QWheelEvent):"""重寫滾輪事件,實現縮放"""mouse_pos_on_scaled_pixmap = event.pos() - self.scaled_pixmap_offset# 根據滾輪方向調整縮放因子if event.angleDelta().y() > 0:zoom_factor = 1.15 # 放大else:zoom_factor = 1 / 1.15 # 縮小# 計算累計縮放比例self.scale_factor *= zoom_factor# 縮放圖片self.scaled_pixmap = self.original_pixmap.scaled(self.size() * self.scale_factor,Qt.KeepAspectRatio,Qt.SmoothTransformation)# 計算鼠標在圖片縮放前后的偏移量delta_offset = mouse_pos_on_scaled_pixmap * (zoom_factor - 1)# 調整圖像顯示位置對容器左上角的偏移量self.scaled_pixmap_offset -= delta_offset# 更新圖像顯示self.update()class MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("Image Viewer with QLabel")self.setGeometry(100, 100, 800, 600)# 創建主窗口布局central_widget = QWidget()self.setCentralWidget(central_widget)layout = QVBoxLayout(central_widget)# 創建 QLabel 用于顯示圖像self.image_label = ImageLabel(self)layout.addWidget(self.image_label)# 創建按鈕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.image_label.set_image(file_path)if __name__ == "__main__":app = QApplication([])window = MainWindow()# 禁用最大化按鈕window.setWindowFlags(window.windowFlags() & ~Qt.WindowMaximizeButtonHint)window.show()app.exec_()
相信經過這兩個示例,特別是后一個以QLabel作為圖片容器實現的示例,可以完全掌握以鼠標為錨點縮放圖片的原理,不但能夠將QGraphicsView以鼠標為錨點進行縮放的效果做得更精確,也能在pyqt以外的其他領域運用自如了。