Featured image of post 【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

紀錄 PyQt5 標籤與顯示 — QLabel、文字顯示、圖片加載、PyQt5 應用開發、使用者介面。

看完這篇文章你會得到的成果圖

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

前言

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。
【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image
https://wongwongnotes.com/posts/python/gui/pyqt/pyqt5-5/

此篇文章的範例程式碼 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day13_scroll_area

以 Qlabel 在 PyQt 中顯示圖片

這篇是延續 Day 12 顯示圖片 zoom in, zoom out 功能的後續開發,
只有 zoom in, zoom out 有時還不足以應付我們處理細節,
因此我們需要一個捲軸,幫助我們能更自由的移動圖片。

UI 設計部份 (UI.py)

新增捲軸欄位

    - 我們先新增一個 Vertical Layout (QVBoxLayout) 位於 Layout 當中,決定好圖片可顯示的範圍。 - 然後在此 Vertical Layout 裡面再新增一個 Scroll Area (QscrollArea) 位於 container 當中,作為可以移動的捲軸範圍。
  1. 在此 Scroll Area (QscrollArea) 當中,再新增一個 Qlabel。作為圖片顯示使用。

    【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

  • 注意順序,先新增 Vertical Layout,疊加上 Scroll Area,再疊加上 Qlabel

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

  • 注意這些物件彼此之間的階層關係,一樣我們可以先修改一些物件名稱,方便我們等等使用

UI 優化:顯示目前圖片的解析度

我們在介面的右下角新增能夠顯示目前圖片的解析度的 Qlabel,
新增這個功能主要是能方便我們能夠確定現在圖片已經被我們縮放到什麼程度了。

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

讀者們可以開始自行設計自己的介面囉,以上為我的示範。

轉換成 UI.py

一樣的編譯指令,我們加上 -x (也可不加),
我們就可以先檢視看看轉換後的視窗是不是跟我們想像的一樣。

轉換 day13.ui -> UI.py

pyuic5 -x day13.ui -o UI.py

執行看看 UI.py 畫面是否如同我們想像

一樣,這程式只有介面 (視覺上的呈現),沒有任何互動功能

  • 看看我們製作出來的介面
python UI.py

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

這樣我們的介面就大致出來囉!

controller 設計部份 (controller.py)

修改 UI.py 的一些程式碼,達成在 QtDesigner 中做不到的事情

我們先觀察一下剛剛在 QtDesigner 中的物件階層關係,

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

其中紅色框框的地方有多出一個我們不要的東西,scrollAreaWidgetContents,
這個東西在 QtDesigner 中預設是會與 QscrollArea 一起被建立,
但實際上因為我們已經很清楚我們需要的是 Qlabel 顯示的圖片,
因此我們直接去改 UI.py 裡面的一些內容。

程式碼中修改與 scrollAreaWidgetContents 相關的內容

我們可以透過搜尋功能幫助我們快速找到相關的段落,這些都是要刪掉的

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

self.scrollAreaWidgetContents = QtWidgets.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 667, 427))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.label.setGeometry(QtCore.QRect(0, 0, 1920, 1080))
self.label.setObjectName("label")
self.scrollArea.setWidget(self.scrollAreaWidgetContents)

我們觀察一下,

  • 基本上前三行都是 scrollAreaWidgetContents 的定義,我們都用不到,直接刪。
  • self.label = QtWidgets.QLabel(self.scrollAreaWidgetContents),
    是藉由 self.scrollAreaWidgetContents 定義出 self.label 的屬性,
    我們不想要這個屬性,但 self.label 是 QLabel 的屬性仍需要被宣告,
    因此我們將他改為 self.label = QtWidgets.QLabel(),單純只宣告他是 QLabel()
  • 後兩行關於 self.label 的定義不需要修改,符合原先的定義即可
  • 最後一行的 self.scrollArea.setWidget(self.scrollAreaWidgetContents),因為我們已經去除了 self.scrollAreaWidgetContents 這個元素,改以 Qlabel 顯示的圖片直接置入 self.scrollArea 當中,因此我們修改成 self.scrollArea.setWidget(self.label)。

修改結果

  • 上面的部份修改完後,結果如下:
# self.scrollAreaWidgetContents = QtWidgets.QWidget()
# self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 800, 400))
# self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.label = QtWidgets.QLabel()
self.label.setGeometry(QtCore.QRect(0, 0, 0, 0))
self.label.setObjectName("label")
self.scrollArea.setWidget(self.label)

為何不使用 self.scrollAreaWidgetContents?

目前測試的結果是不會成功的顯示出捲軸,可能的原因是因為 Qlabel 才有存在超過視窗範圍的大小,而 self.scrollAreaWidgetContents 作為容器,並沒有辦法以超過的大小觸發 self.scrollArea 的捲軸事件,因此功能失效。
不過這部份原因目前只是我的猜測,總之捲軸的功能是無法正常運行的。

從 UI.py 中找出物件名稱

這次除了 day12 既有的功能之外,我們新增了一些物件,

  • self.btn_zoom_in、self.btn_zoom_out:同 day12 的 zoom in, zoom out 的按鈕
  • self.label:顯示圖片的 Qlabe
  • self.scrollArea:圖片縮放的範圍
  • self.img_shape:作為 UI 優化新增的 label,我們可以從這裡觀察目前圖片的解析度。

取得名稱後,去修改 controller.py

我們繼續修改我們 day12 的程式碼

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFileDialog
import cv2

from UI import Ui_MainWindow

class MainWindow_controller(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__() # in python3, super(Class, self).xxx = super().xxx
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setup_control()


    def setup_control(self):
        # TODO        
        self.img_path = 'cat.jpg'
        self.ui.btn_zoom_in.clicked.connect(self.func_zoom_in) 
        self.ui.btn_zoom_out.clicked.connect(self.func_zoom_out)
        self.ui.scrollArea.setWidgetResizable(True)
        self.ui.label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
        # self.ui.label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) # 將圖片置中
        self.display_img()

    def display_img(self):
        self.img = cv2.imread(self.img_path)
        height, width, channel = self.img.shape
        bytesPerline = 3 * width
        self.qimg = QImage(self.img, width, height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
        self.qpixmap = QPixmap.fromImage(self.qimg)
        self.qpixmap_height = self.qpixmap.height()
        self.ui.label.setPixmap(QPixmap.fromImage(self.qimg))

    def func_zoom_in(self):
        self.qpixmap_height -= 100
        self.img_resize()

    def func_zoom_out(self):
        self.qpixmap_height += 100
        self.img_resize()

    def img_resize(self):        
        scaled_pixmap = self.qpixmap.scaledToHeight(self.qpixmap_height)
        print(f"current img shape = ({scaled_pixmap.width()}, {scaled_pixmap.height()})")
        self.ui.img_shape.setText(f"current img shape = ({scaled_pixmap.width()}, {scaled_pixmap.height()})")
        self.ui.label.setPixmap(scaled_pixmap)

setup_control() 修改的部份

與 day12 的不同是,我們主要新增了這兩行

self.ui.scrollArea.setWidgetResizable(True)
self.ui.label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
# self.ui.label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) # 將圖片置中
  • self.ui.scrollArea.setWidgetResizable(True):這行在 Qtdesigner 中也可以設定,預設是 False,我們將他改為 True,讓我們的 scrollArea 可以被捲動
  • self.ui.label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop):將我們的圖片往左上角對齊,往左上角對齊有兩個好處,一個是我們之後如果要進行圖像處理,這樣算座標會非常方便。

但是如果為了好看,想讓圖片置中,可以改為以下敘述:

self.ui.label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)

img_resize() 的部份 (原 day12 resize_image())

因為我們新增了 UI 優化的功能,稍微想一下就可以知道,
這段程式碼基本上會跟著我們圖片變化一起改變,
因此我們把「顯示圖片現在解析度」的功能新增在此處。

  • print(f"current img shape = ({scaled_pixmap.width()}, {scaled_pixmap.height()})"):取得現在圖片高度、寬度並顯示在 terminal 當中
  • self.ui.img_shape.setText(f"current img shape = ({scaled_pixmap.width()}, {scaled_pixmap.height()})"):取得現在圖片高度、寬度並顯示在 Qlabel 當中

執行結果

照我們 day5 的程式架構,我們執行

python start.py

【PyQt5】Day 13 - 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV) PyQt5 scrollable image

Reference

使用 Hugo 建立
主題 StackJimmy 設計