➣ Reading Time: 12 minutes

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

此篇文章的範例程式碼 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day28-30_final_project

之前內容的重點複習 (前情提要)

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

設計我們的 UI

雖然原本我有想直接拿 Day17 的內容繼續改,
後來覺得架構上還不夠漂亮,最後決定乾脆直接砍掉重練比較快哈哈哈。

既然要搞就一次搞到最大吧!

這次我設計的 final project UI 初版長這樣

欸等等先別吐血啊!!! 這 UI 不是一天搞出來的啊!!
UI 就請大家自己慢慢刻哈哈哈哈哈!!!

(你說突然這也突然跳太高階了吧?!
其實只是設定一些顏色而已XD,之前因為這個相對簡單就沒特別介紹)

排版控制 Layout 系列

如果還有機會的話有空在介紹,不過這邊的內容多半是用於排版,
所以也難能以單一例子舉出 demo 在幹嘛,
總之可以自己玩玩看 Layouts 這一塊,
先「拉一個 Layout」,再把「要排版的元件丟進去」,就對自動對整齊了!

  • Layouts 系列:

設定字體顏色,背景顏色的地方位於 styleSheet

使用的應該是類似 css 語法? 我不確定XD
總之有興趣可以直接參考下面的延伸閱讀進行修改,
設定位置在 Qt desinger 的這邊

延伸閱讀:給想研究更多怎麼設定顏色的人,請參考 Qt Style Sheets Examples

設定 Logo 的地方

Qlabel 就是一個我們可以輸入圖片的地方,
透過右邊的「…」,選擇一張我們要的圖片,就可以直接把 LOGO 嵌入 UI 中了。

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

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

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

讚到不行XDDDD (自己講

程式架構篇

這次的 final project 真的內容會做太多,如果沒有好好規劃一下架構,
很有可能寫出來的東西會一團亂XDDD
所以我們要先趁今天來規劃,明天再來慢慢把功能實現

先從大方向切入,我們有那些大的「重要元素」?

依照 day5 的設計,我們至少也會有

  • UI (作為前端顯示介面)
  • controller (做為後端邏輯控制)
  • start (啟動用,沒什麼好討論的)

而 UI 因為我們在 Qtdesigner 已經設計得夠複雜了,也不太需要另外寫程式,
所以基本上在經由 pyuic 的轉換後,就已經完成了 UI.py

pyuic5 -x day28.ui -o UI.py

controller 是我們這邊要討論的重點,在我們的 photoshop 後端邏輯裡面,
我之前在 day17 實作的最終版是把「圖像+圖像變化方法」寫在同一個 class 當中,
我們如果繼續讓我們的「變化方法增加」,不適不能這樣實作,
但最終我們會有一個超級巨大的 class 共同實作 「圖像+圖像變化方法」,
於是我決定把「變化方法」拆出去獨立一個 class,
使得「圖像」與「變化方法」分開成兩套獨立機制,而能互向協助。

這也符合 design pattern 的 Interface Segregation Principle(ISP) 介面隔離原則
以遊戲來說,我們也可以舉例:
我們大可以把一個角色的所有「屬性、裝備、技能」全部都包在這個角色當中,
但如果另外一個玩家也玩了同一個職業,我們何必把技能「整個複製進去另外一個角色當中呢?
於是我們可以把「技能」這個介面分離出去,讓有需要的類別再去呼叫這個介面即可。

因此,這時候就能透過這樣的拆分方式,把我們實作的彈性與擴充功能的彈性提升。

  • 舉例:

套用 design pattern 前

套用 design pattern 前,基本上這個設計沒什麼問題,
硬要說缺點只是維護職業技能時,「需要一個個角色去調整內部的技能

套用 design pattern 後 (使用 Interface Segregation Principle(ISP) 介面隔離原則)

套用 design pattern 的 Interface Segregation Principle(ISP) 介面隔離原則後,
我們把技能獨立出來維護,這樣的好處就是我們可以調整一次職業技能,
所有的角色都能得到修正!

而這邊我們也要做同樣的事情,我們要把「修改圖片的方法」這個介面獨立出來!

於是整理一下,得到我們目前想設計的架構

獨立「圖片本身」與「圖片處理方法」,
另外因為滑鼠事件的資訊比較特別,需要從圖片上獲取,所以我們也另外獨立出來處理

【重要】抓出誰會是觸發事件的 trigger

第一次設計這個系統的時候,就碰到一個最關鍵的問題,
沒有提早先想好架構就開始寫,導致繞了很多彎路」,

應該要先想好再下去寫的問題就是,在這個系統裡面,「什麼事情會是觸發事件的 trigger?

如果我們不知道「什麼事件會被使用者觸發」,
就不知道要從哪個點開始啟動後續的修改」,

我一開始就是瘋狂地開始寫功能XDD,然後沒注意誰呼叫誰,
結果功能都有了,但是 trigger 位置寫在錯誤的 class,
所以要重搞順序XDD

以結論來說,這個系統的 trigger 位置如下

所以在寫系統時,我們需要特別注意,
我們箭頭指的地方就是「會觸發事件的 trigger」,
凡經過此處的程式都是「一次修改的起點」,引導我們進行往後的修改。

圖形修改介面 (interface) 設計

基本上我們設計的「修改圖片」都有符合一些同樣的原則,
我們可以使用「介面繼承」的方式來實現,
為了達到這樣的效果,我們需要 「import abc」 這個 python 酷酷的 library,
細節的部分我會再另外寫一篇文章,這邊我們先直接實作。

註:冷知識(?) abc = the infrastructure for defining 「abstract base classes (ABCs)」 in Python
簡單來說,就是「抽象的類別 (class)」定義

我們預計設計的架構如下,一共會有三層,底下會一層層介紹:

介面的大祖宗 (method_interface),一切介面方法從此開始實作

我們可以大致歸納我們全部的變化介面都會有兩個基本的要素:

  • 初始化參數:__init__
  • 更新圖片: update_img

另外,我們強制這兩個介面在繼承後都必須被定義,
所以我們透過「@abc.abstractmethod」,定義這個抽象的方法 (未實作功能),
以及「return NotImplemented」,如果使用者繼承介面後未定義這個函數,
會跳 「NotImplemented error」,強制跳錯 (逼使用者一定要定義介面內容)。

import abc

class method_interface(abc.ABC):
    @abc.abstractmethod
    def __init__(self):
        return NotImplemented

    @abc.abstractmethod
    def update_img(self):
        return NotImplemented 

介面的父母 (滑條方法介面 slider_method_interface, 畫筆方法介面 pen_method_interface)

我們會實作的「圖像變化方法」,能夠產生變化的方法大概有兩種,
一種是滑條類,另一種是畫筆類。

這兩類在實作時,都會繼承我們的介面大祖宗 (method_interface),
然後再分別基於滑條的特性與畫筆的特性,實作各自的介面。

這所有的介面方法都屬於「圖片變化方法」,所以我們會強烈推薦用繼承的方法撰寫。
會比起一個個慢慢定義有更好的架構與維護性
(維護一個父類別,也許底下的所有子類別都能全部受惠到這次的更動。不用一個個慢慢改。)

最大的差別就是:

  • 滑條拉回去後,可以復原 (不會是破壞原圖的變動)
  • 畫筆畫下去後,不可復原 (會破壞原圖的變動)

因此這兩個方法我們要分開實作,實作細節一樣我們明日再提,
今天光是講大架構就已經夠累了。

各個細部的方法實作

在看該方法是「滑條方法介面 slider_method_interface」或是「畫筆方法介面 pen_method_interface」後,
我們就可以開始實作細部功能了,這部分的實作細節我們就明日在提吧。
(今天光是講完大架構相信大家已經都累了XDDD)

最終系統架構圖

這個是舊版,滑鼠那部分因為是新加的,目前還沒更新上去,
不過相信上面已經說明得相當完整了,應該能大約類推出他在圖形上會呈現的細節

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