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

➣ Reading Time: 16 minutes

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

前言

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。

【PyQt5】Day 5 – 開始來設計我們的 controller.py,改以「程式角度」來說明如何建立 PyQt 的系統

此篇文章的範例程式碼 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)

新增捲軸欄位

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

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

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

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

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

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

轉換成 UI.py

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

轉換 day13.ui -> UI.py

pyuic5 -x day13.ui -o UI.py

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

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

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

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

controller 設計部份 (controller.py)

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

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

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

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

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

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

Reference

⭐Python PyQt5 相關文章整理⭐:
⭐基礎知識與架構篇⭐:
1.【PyQt5】Day 1 – 安裝 PyQt,建立自己的第一支 PyQt5 程式
2.【PyQt5】Day 2 – 利用 Qt designer 建立第一支有自己介面的 PyQt5 程式
3.【PyQt5】Day 4 – 重要的 Qt 程式邏輯觀念,務必先有此觀念後面才會懂自己在幹嘛
4.【PyQt5】Day 5 – 開始來設計我們的 controller.py,改以「程式角度」來說明如何建立 PyQt 的系統
⭐基本元件應用篇⭐:
1.【PyQt5】Day 6 – 我們的第一個 output 手段 – Qlabel
2.【PyQt5】Day 7 – 我們的第一個 input 手段 – QPushButton
3.【PyQt5】Day 8 – 我們的第二個 input 手段 – QLineEdit
4.【PyQt5】Day 9 – 以 QLineEdit, QTextEdit, QPlainTextEdit 作為文字的輸入
6.【PyQt5】Day 14 - 使用 QSlider 製作可拖曳的滑條
8.【PyQt5】Day 19 - 使用 QProgressBar,製作進度條的功能
⭐介面系統控制篇⭐:
5.【PyQt5】Day 10 – 以 QFileDialog 讀取系統的檔案、資料夾
7.【PyQt5】Day 18 / Project 使用 QTimer,自製碼表(計時器) PyQt5 stopwatch DIY
1.【PyQt5】Day 20 - PyQt 最重要的 QThread 概念 / 為什麼 windows, mac, ubuntu (linux) 程式會「沒有回應」?
2.【PyQt5】Day 21 – 透過 PyQt 實現滑鼠監聽總整理,完全掌握滑鼠控制 (listen mouse)
3.【PyQt5】Day 22 – PyQt 視窗的個性化/屬性控制 setWindowFlags,禁止放大縮小、永遠顯示於最上層/最下層
4.【PyQt5】Day 23 – 使用系統內建的調色盤 QColorDialog,來替我們選擇顏色 QColor (Color Picker)
5.【PyQt5】Day 24 / Project 偵測滑鼠目前指示顏色的小工具 (滴管工具), 利用 QCursor 偵測滑鼠, QApplication 取得截圖
⭐影像處理篇⭐:
1.【PyQt5】Day 11 – 以 Qlabel 在 PyQt 中顯示圖片 (基於 QImage 使用 OpenCV)
2.【PyQt5】Day 12 – 建立一個可以縮放圖片大小的顯示器 (基於 QImage 使用 OpenCV)
3.【PyQt5】Day 13 – 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV)
4.【PyQt5】Day 15 / Project 與檔案功能整合,製作出可讀取圖片並可縮放的 UI 介面 (使用 PyQt + OpenCV)
5.【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理
6.【PyQt5】Day 17 / Project 製作標註 roi 工具, 開始導入 OpenCV 作為繪圖引擎, 在圖上畫點並顯示座標
⭐project 篇⭐:
1.【PyQt5】Day 25 / Project 自己做一個影片播放器 DIY video player (結合 PyQt + OpenCV)
2.【PyQt5】Day 26 / Project 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)
3.【PyQt5】Day 27 / Project 製作影片 ROI 標註工具 (PyQt 結合 OpenCV 在圖上畫點畫線)
4.【PyQt5】Day 28 / final project – 1 來搞一個自己的 photoshop 吧!UI 篇 + 純程式架構篇 (結合 PyQt + OpenCV)
5.【PyQt5】Day 29 / final project – 2 來搞一個自己的 photoshop 吧!後段程式細節篇 (結合 PyQt + OpenCV)
6.【PyQt5】Day 30 / final project – 3 來搞一個自己的 photoshop 吧!把每個方法封裝起來製作出還原功能吧! (結合 PyQt + OpenCV)
⭐打包程式篇⭐:
1.【PyQt5】Day 3 – 用 pyinstaller 將 python 程式打包,把每天的成果分享給你的親朋好友
⭐【喜歡我的文章嗎? 歡迎幫我按讚~ 讓基金會請創作者喝一杯咖啡!
如果喜歡我的文章,請幫我在下方【按五下Like】 (Google, Facebook 免註冊),會由 「LikeCoin」 贊助作者鼓勵繼續創作,讀者們「只需幫忙按讚,完全不用出錢」哦!

likecoin-steps
預設圖片
Howard Weng

我是 Howard Weng,很多人叫我嗡嗡。這個網站放了我的各種筆記。希望這些筆記也能順便幫助到有需要的人們!如果文章有幫助到你的話,歡迎幫我點讚哦!

文章: 673

5 則留言

★留個言吧!內容有誤或想要補充也歡迎與我討論! 

  1. 版主您好

    主旨: [關於Day13的UI.py部分]

    關於scrollAreaWidgetContents之所以會沒辦法出現捲軸,後來看了一些資料探討之後整理出這些原因。
    (希望能填填坑,幫大家少走一點錯路。畢竟中文的Qt教學真的太少了QQ)

    [scrollAreaWidgetContents部分]
    *原因探究*
    第一個原因,是因為QtDesigner中scrollAreaWidgetContents的最大大小設定只能比scrollArea小一點點。(也就是說scrollAreaWidgetContents永遠不能比scrollArea大QQ)

    : 那只要去Ui.py檔調一下大小就好了是嗎?

    也不行,因為第二個原因:
    scrollAreaWidgetContents在設定大小時,預設是使用setGeometry()這個函式。
    像本文中這樣:

    self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 800, 400))

    這個函式好像會自動把scrollAreaWidgetContents這個次元件,resize成適合其主元件的大小。如此一來就永遠沒辦法觸發捲軸事件。

    *改善方法*
    要改善這個方法,後來發現很簡單。只要改成setFixedSize()這個可以固定scrollAreaWidgetContents元件大小的函式,並且在一開始把大小設定得比scrollArea大,即可在執行UI.py檔時看捲軸。例如:

    self.scrollAreaWidgetContents.setFixedSize(1000, 1000)

    註: 這裡只需要大小參數,他的擺放位置似乎用scrollArea當作參考點。

    [Vertical Layout]
    另外不太清楚為什麼要加Vertical Layout,後來發現scrollArea可以直接加上去(?

    [controller部分]
    為了讓scrollAreaWidgetContents可以隨圖片大小調整,有更動一下img_resize()的部分。

    我把程式碼放在github,給版主參考看看(要注意變數名、圖檔名都有重設噢~)
    https://www.wongwonggoods.com/python/pyqt5-13/

    希望這些可以幫助到之後有興趣的讀者~
    也還是得感謝一下,這個30天挑戰系列寫得真的很好! 幫助自己超多

    • 有你們這樣的建議才是最好的回饋哈哈哈哈! 自己一個人寫30天能夠看的面向太有限了,
      真的感謝你們幫忙!!

      我最近再來找時間修改一下~~
      (最近比較忙,有些自己的筆記都還沒更上來XDD)