➣ Reading Time: 20 minutes

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

前言

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

此篇文章的範例程式碼 github

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

UI 設計部份 (UI.py)

我們今天要把 Day 10 的讀檔功能Day 14 的滑條功能
整合進 Day 13 的最終成果 當中。

  • 因此我們會從 Day 13 接下去改後續的功能。

我設計的介面如同上圖,
部份想法如下:

  • 除了 zoom in, zoom out 可縮放圖片大小之外,也可用滑條改變,我預期
    • 50 -> 100%
    • 0 -> 10%
    • 100 -> 1000%
  • 推算公式可以得到 y = 10^((x-50)/50)

推算公式的過程,我們把所有數值先正規化到 -1~1 間,就會很好推公式了
我們把 10% -> 0.1,100% -> 1,1000% -> 10
可參考一下的表,就是我們正規化後反推公式的過程。
最後可得上方公式 y = 10^((x-50)/50)

progress bar 正規化
050100
-50050
-101
ratio 正規化
10%100%1000%
10^-110^010^1
-101

轉換成 UI.py

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

轉換 day15.ui -> UI.py

pyuic5 -x day15.ui -o UI.py

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

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

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

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

controller 設計部份 (controller.py)

從 UI.py 中找出物件名稱

button 類

  • btn_open_file: 開啟檔案用按鈕
  • btn_zoom_in: 放大用按鈕
  • btn_zoom_out: 縮小用按鈕

label 類

  • label_ratio: 顯示現在圖片縮放比例
  • label_file_name: 顯示現在檔名
  • label_img_shape: 顯示現在/原來圖片大小
  • label_img: 顯示圖片

silder 類

  • silder_zoom: 控制圖片大小

scrollArea 類

  • scrollArea: 圖片縮放區域

同 day13 的 scrollArea 說明,我們一樣需要刪除 scrollAreaWidgetContents 的部份

self.scrollArea = QtWidgets.QScrollArea(self.verticalLayoutWidget)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setObjectName("scrollArea")
# self.scrollAreaWidgetContents = QtWidgets.QWidget()
# self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 937, 527))
# self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
# self.label_img = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.label_img = QtWidgets.QLabel()
self.label_img.setGeometry(QtCore.QRect(0, 0, 941, 521))
self.label_img.setObjectName("label_img")
# self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setWidget(self.label_img)
  • 可能會有新增與調整的 scrollArea 片段

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

我們繼續修改我們 day13 的程式碼,但我們這次發現我們的程式碼越來越大了,
是時候該做點封裝了,我們決定將圖片有關的功能封裝至另外一個檔案 img_controller.py

另外封裝專門處理圖片的 img_controller.py

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

class img_controller(object):
    def __init__(self, img_path, label_img, label_file_path, label_ratio, label_img_shape):
        self.img_path = img_path
        self.label_img = label_img
        self.label_file_path = label_file_path
        self.label_ratio= label_ratio
        self.label_img_shape = label_img_shape
        self.ratio_value = 50
        self.read_file_and_init()
        self.__update_img()

    def read_file_and_init(self):
        try:
            self.img = cv2.imread(self.img_path)
            self.origin_height, self.origin_width, self.origin_channel = self.img.shape            
        except:
            self.img = cv2.imread('sad.png')
            self.origin_height, self.origin_width, self.origin_channel = self.img.shape    

        bytesPerline = 3 * self.origin_width
        self.qimg = QImage(self.img, self.origin_width, self.origin_height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
        self.origin_qpixmap = QPixmap.fromImage(self.qimg)
        self.ratio_value = 50        
        self.set_img_ratio()

    def set_img_ratio(self):
        self.ratio_rate = pow(10, (self.ratio_value - 50)/50)
        qpixmap_height = self.origin_height * self.ratio_rate
        self.qpixmap = self.origin_qpixmap.scaledToHeight(qpixmap_height)
        self.__update_img()
        self.__update_text_ratio()
        self.__update_text_img_shape()

    def set_path(self, img_path):
        self.img_path = img_path
        self.read_file_and_init()
        self.__update_img()

    def __update_img(self):       
        self.label_img.setPixmap(self.qpixmap)
        self.label_img.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)

    def __update_text_file_path(self):
        self.label_file_path.setText(f"File path = {self.img_path}")

    def __update_text_ratio(self):
        self.label_ratio.setText(f"{int(100*self.ratio_rate)} %")

    def __update_text_img_shape(self):
        current_text = f"Current img shape = ({self.qpixmap.width()}, {self.qpixmap.height()})"
        origin_text = f"Origin img shape = ({self.origin_width}, {self.origin_height})"
        self.label_img_shape.setText(current_text+"\t"+origin_text)

    def set_zoom_in(self):
        self.ratio_value = max(0, self.ratio_value - 1)
        self.set_img_ratio()

    def set_zoom_out(self):
        self.ratio_value = min(100, self.ratio_value + 1)
        self.set_img_ratio()

    def set_slider_value(self, value):
        self.ratio_value = value
        self.set_img_ratio()

讀者看到會不會看到突然覺得這個程式難度整個飛起來…
但其實這些都是我們前幾天學的東西哦,只是我們加入了一點物件導向的寫法,
我們把每一個功能都分裝好,不要讓所有程式碼集中到 controller.py,
不然 controller.py 太過強大,萬事都能做到,我們還真的不知道,
如果今天要改一個功能,要去 controller.py 的哪找…
(就跟圖書館什麼都有,只是你能不能有效率的快速找到你要的東西一樣)

主要我們分裝成這些函數,並遵守我自己定義的規則:

  1. set_instance_name:使用者可以 call,去修改一些想要的變化,並在此實作複雜功能與算法
  2. __update_instance_name:private function,不希望使用者去 call,主要只負責單純的更新 info,而不實作任何複雜功能或算法

因此我們有:

  • init():初始化
  • read_file_and_init():實作讀取圖片檔案,並實作圖片初始化
  • set_img_ratio():設定圖片比率,並實作變化
  • set_path():更換檔案時,更新圖片路徑
  • set_zoom_in():設定 zoom in 功能
  • set_zoom_out():設定 zoom out 功能
  • set_slider_value():設定縮放功能的那條 bar
  • __update_img():更新圖片
  • __update_text_file_path():更新圖片路徑的文字
  • __update_text_ratio():更新圖片縮放比率的文字
  • __update_text_img_shape():更新圖片大小的文字

controller.py 的部份

from PyQt5 import QtCore 
from PyQt5.QtWidgets import QMainWindow, QFileDialog

import time
import os

from UI import Ui_MainWindow
from img_controller import img_controller

class MainWindow_controller(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):
        self.file_path = ''
        self.img_controller = img_controller(img_path=self.file_path,
                                             label_img=self.ui.label_img,
                                             label_file_path=self.ui.label_file_name,
                                             label_ratio=self.ui.label_ratio,
                                             label_img_shape=self.ui.label_img_shape)

        self.ui.btn_open_file.clicked.connect(self.open_file)         
        self.ui.btn_zoom_in.clicked.connect(self.img_controller.set_zoom_in)
        self.ui.btn_zoom_out.clicked.connect(self.img_controller.set_zoom_out)
        self.ui.slider_zoom.valueChanged.connect(self.getslidervalue)

    def open_file(self):
        filename, filetype = QFileDialog.getOpenFileName(self, "Open file", "./") # start path        
        self.init_new_picture(filename)

    def init_new_picture(self, filename):
        self.ui.slider_zoom.setProperty("value", 50)
        self.img_controller.set_path(filename)        

    def getslidervalue(self):        
        self.img_controller.set_slider_value(self.ui.slider_zoom.value()+1)

因為我們把大部分的功能都封裝在 img_controller.py 裡面了,
因此現在我們只需要單純的「from img_controller import img_controller」,
就能使用 img_controller 的 「set 開頭相關的函數」搞定所有圖片相關的功能。

這邊我們在 setup_control() 實作的有:

def setup_control(self):
    self.file_path = ''
    self.img_controller = img_controller(img_path=self.file_path,
                                         label_img=self.ui.label_img,
                                         label_file_path=self.ui.label_file_name,
                                         label_ratio=self.ui.label_ratio,
                                         label_img_shape=self.ui.label_img_shape)

    self.ui.btn_open_file.clicked.connect(self.open_file)         
    self.ui.btn_zoom_in.clicked.connect(self.img_controller.set_zoom_in)
    self.ui.btn_zoom_out.clicked.connect(self.img_controller.set_zoom_out)
    self.ui.slider_zoom.valueChanged.connect(self.getslidervalue)

分別有設定圖片路徑,初始化 img_controller,設定按鍵功能,
因為 button 因為吃的也是 function,可以直接 call img_controller 的函數來使用,
而開檔案與滑條的部份,我們一樣另外使用 open_file()、getslidervalue() 來實作。

特別注意圖片初始化問題,我們另外用 init_new_picture() 來處理

因為有時候開新的檔案,就會有一大堆東西要初始化,一個疏忽就會讓人非常頭痛,因此我們也另外使用 init_new_picture(),把所有應該要初始化的功能都集中在這邊。 (例如像 bar 需要回到中間 50 的地方)

另外把程式封裝,還有一個最大的好處就是更容易 debug,
我們因為功能切的很細,如果我們今天看畫面上「少了哪一個功能」,
我們只需要去把對應的功能呼叫回來,問題就解決了。
如果是沒有封裝好的情況,可能會要開始在「function 海當中尋找對應的那一行功能」,會大幅減少解 bug 的效率。

執行結果

照我們 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 作為文字的輸入
5.【PyQt5】Day 10 – 以 QFileDialog 讀取系統的檔案、資料夾
⭐影像處理篇⭐:
1.【PyQt5】Day 11 – 以 Qlabel 在 PyQt 中顯示圖片 (基於 QImage 使用 OpenCV)
2.【PyQt5】Day 12 – 建立一個可以縮放圖片大小的顯示器 (基於 QImage 使用 OpenCV)
3.【PyQt5】Day 13 – 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV)
⭐打包程式篇⭐:
1.【PyQt5】Day 3 – 用 pyinstaller 將 python 程式打包,把每天的成果分享給你的親朋好友
⭐【喜歡我的文章嗎? 歡迎幫我按讚~ 讓基金會請創作者喝一杯咖啡!
如果喜歡我的文章,請幫我在下方【按五下Like】 (Google, Facebook 免註冊),會由 「LikeCoin」 贊助作者鼓勵繼續創作,讀者們「只需幫忙按讚,完全不用出錢」哦!

likecoin-steps