從 PyQt 入門學寫 Python

[2018-04-03 火 10:03] 修正一些錯誤觀念
[2014-09-22 月 22:33] 更新:新增 parent 說明

學了一個暑假的 PyQt,決定還是應該把詳細的撞牆筆記寫出來,如果您想學 PyQt,希望這篇對您會有點幫助。以下概念如有謬誤敬請指正:)

  • 那入門學 PyQt 要不要順便一起學 QML 呢?我個人的建議是:不要
  • [2018-04-03 火 10:06] 補充:QML 是我見過最糟糕的程式語言之一,滿滿的雷,是個實做殘缺不全又飽含許多讓你開發到抓狂的異常行為的 ECMAScript/JavaScript,我現在連寫 Qt/C++ 都已經放棄 QML 了、繼續寫 QWidget 去。
    由於我之前沒有 OOP 基礎,所以一開始的門檻還是有點高,也因此這部份我會講得比較清楚又囉唆一點,讓沒有 OOP 基礎的人也能夠輕易看懂。不過學 PyQt 其實不需要先學會 Python,就跟(聽說)學 Rails 並不須要先學會 Ruby 一樣。因為PyQt 會用到的 Python 特性非常偏頗,而且細節頗多,與其說在寫 Python 還不如說就是在寫 PyQt。最難的就是搞懂並記住這些特性與細節(且有些細節在一般 Python 程式裡應該也很少用),基本上先把幾個基本概念和常用 pattern 記住就對了。
  • class 裡定義的 function 叫做 method,class 裡定義的 variable 就是 attribute。
  • instance 中文翻成「實體」,一個 class 經常要先建立 class 的 instance 才能開始使用,因為要建立 instance 才會載入 class 的 __init__ 部份。

一開始請先讀 amulet 大神寫的這串 PyQt 入門教學,寫得非常好:

接下來我不會從頭講,因為上述的教學講得已經很好了,然而我想把我在學的時候遇到的一些不懂也不知該怎麼問的概念部份重新講一次,所以現在這篇應該算是上述文章的補充講義。所以如果您讀了上面的那些教學有什麼沒看懂的部份,就請回來看我寫的這些更詳細的筆記,搭配看應該是非常好理解的(尤其 class 的部份)。

這裡需要的基礎知識主要是著重在 Python 的 class 部份,不過您只需要有最基礎的 Python class 概念就可以看這幾篇文章和本篇學 PyQt 了。

除此之外也可以參考 Python 的變數命名方式,寫起 PyQt 起來能省下不少讀 code 的功夫。

Import

module 有兩種最常見的 import 方式,第一種是:

1
2
from PyQt4.QtCore import *
from PyQt4.QtGui import *

因為 * 會將 module 內的 namespace 拿來直接蓋過目前環境的 namespace。所以使用這種方法的話,要呼叫 module 內的東西只需要寫成這樣: app = QApplication(sys.argv)

另二種是:

1
from PyQt4 import QtCore, QtGui

這種方法的話,要寫成這樣: app = QtGui.QApplication(sys.argv)

第一種方法可以少打幾個字,雖然一般來說會有 namespace 污染的問題,但由於 Qt 的命名方式不管什麼都是 Q 開頭,幾乎不可能跟你寫的東西撞名,所以其實沒啥問題。
第二種方法優點是是不會污染到目前環境的 namespace(不過 PyQt 的 class 都是以 Q 開頭,也幾乎不太可能會污染到你原有的 class 名稱),缺點是要多打幾個字。

我自己是用第二種方式,原因是 Jedi 這套 Python auto-complete 外掛似乎要用第二種才能運作。以下範例皆採用第二種方式。

Class

Qt 中:
一切的物件都是從 QtCore.QObject 繼承而來(如 QShortcut);
一切的 GUI 物件都是從 QtGui.QWidget 繼承而來(如 QLabel)。
別把 Object 跟 Widget 看錯了!

1
2
3
class MyWidget(QtGui.QWidget):
def __init__(self, parent = None):
super(MyWidget, self).__init__(parent)
  1. 第一行的語法 class CLASS_NAME(INHERT)INHERT 參數是代表「現在所定義的這個 class 該繼承 INHERT 這個 class」,簡單來說就是 INHERT 這整個 class 的所有內容拷會貝一份,複製到你現在定義的這個 class 內

    這裡是先方便你理解才說用「拷貝一份」,其實 OOP 繼承時完全不會複製 class 的內容,真的那樣複製的話太浪費資源也太沒效率。
    不過初學時你還是可以先這樣想,其餘的以後再說。

    • 以上面的 code 為例,就是我們定義了一個 class 叫做 MyWidget ,並繼承了 QtGui.QWidget 這個 class。
    • 以 OOP 的術語來說,MyWidget 就是 QWidget的 child class(子類別,或者語意更精確的「衍生類別」 “derived class“),QWidgetMyWidget的 parent class(父類別 ,或者更精確的「基礎類別」 “base class“)。
  2. 我們上面繼承了QtGui.QWidget,所以現在我們可以來自訂這個 QWidget、改成我們想要的元件了。
    第二行def __init__(self, parent = None): 就是在「重新定義」 QtGui.QWidget__init__

    • __init__ 這個 method 是 建立 class 的instance實體後,會自動呼叫的 method。
      • 在 OOP 的術語中,這種 method 叫做 constructor(建構式)。
      • 他定義了這個 class 一開始應該做哪些事,像是 PyQt 中每個 class 的 __init__ 就是在建構 Qt 的元件本身。但我們現在要自訂它,例如更改 QWidget 的背景色、尺寸等等。

        除了背景色、尺寸外,還有這個 QWidget 要放什麼類型的 layout,layout 裡面要放哪些 QWidget,以及預設要呼叫哪些 method、預設有哪些 attribute 等,都是在 __init__ 這裡自訂。因為他就是一建立 該 class 的 instance 時,會初始化載入的部份。

    • self 是很怪很怪的東西,他指的就是這個 class 的 instance 本身…總之 Python 裡面在 class 中 定義 method 時,第一個參數一定要加上 self,再來後面接的才是我們(在其他語言中)一般認知上的 function/method 參數,先狠狠地記住這點就對了。

      其實也不全然是這樣,還有 @static_method@class_method 這兩種東西的寫法就通常不會寫self或者根本不用刻意加第一個參數,不過這你大可以先無視,以後熟悉 OOP 後再去查這兩種東西的用法。

    • parent = None 代表 __init__ 中, parent 這個 argument參數 的預設值是 None。也就是說,我們之後在呼叫 MyWidget() 這個 class 時,如果沒有指定 parent,那 parent 這個 argument 就會預設被賦予 None 這個值。
      parent 的值在 PyQt 裡有一個重要角色,就是決定目前 QWidget 是否是另一個 QWidget 的 child object,如果 parentNone ,就代表他沒有 parent,是一個獨立的物件。
      稍後會提到 parent 在 PyQt 裡的用法。

      注意,這裡的 parent 為 Qt 術語中的 parent,不是 OOP 那個 parent class 的 parent。

  3. super(MyWidget, self).__init__(parent) 是平常 Python 可能很少用(好啦除非你整天用到繼承),但 PyQt 裡一直狂用的重要東西。要解釋得分成多部份:

    • 我們讓 MyWidget 繼承 QtGui.QWidget 後,MyWidget 的整個 class、 包含 __init__ 都跟 QtGui.QWidget 一模一樣。
    • 但我們為了要自訂 MyWidget ,而使用到了 def __init__()一旦定義 def 了一個「已經存在的 method」,那 MyWidget 所繼承而來的該 method 將會會直接被忽略並覆蓋過去。
    • 因此,def __init__() 會導致 MyWidget__init__ (從 QtGui.QWidget 繼承而來的 __init__ ) 被整個清空,這樣一來該 Qt 元件就沒辦法成功被初始化(因為載入 Qt 元件的部份都是被定義在 __init__ 中)。
    • 所以我們為了要能夠自訂 MyWidget 中的 __init__ ,又能夠保有原本從 QtGui.QWidget 繼承而來的 __init__,所以在我們 def __init__() 之後,就立刻重新載入一次原本 QtGui.QWidget 內的 __init__。要做到這件事,有兩種方法:
      1. 直接呼叫 QtGui.QWidget().__init__()
      2. 使用 super() 來作跟樓上一樣的事情,有兩種寫法:
        • super(MyWidget, self).__init__()super(MyWidget, self) 指的就是「MyWidget 這個 class 所繼承的 class」,在我們的例子中指的正是 QtGui.QWidget
          所以整句的意思就是「呼叫 QtGui.QWidget__init__()
        • Python 3 開始有更簡單的寫法:直接寫 super().__init__() 。我自己後來也都這樣寫,傳統 Python2 的寫法根本是喪心病狂般難記難寫。
  • 如果你打算把這個 QWidget 物件作為視窗使用(或者本身就是視窗,如QDialog)記得加上 parent = None。因為以後要呼叫這個自訂的 MyWidget 時,就可以指定 parent,這樣 parent 一旦關閉,MyWidget 也會自動關閉。例如:
1
2
child_window = QtGui.QMainWindow(main_window) # main_window 關閉時,child_window 也會被自動關閉。
main_window = QtGui.QMainWindow()

還有一點概念非常重要,沒有 parent (且也沒有被放在 window 或者 layout 中) 的任何 QWidget 就會變成一個獨立的視窗。
要建立視窗時要注意一點,如果要在 class 的 method 裡面建立獨立視窗的 instance,記得加上 self,例如self.label = QLabel() ,不可直接 label = QLabel(),不然該會視窗跑不出來。

self

self 的概念很重要,什麼時候該用?看他需不需要被跨 method 存取。如果只是上面的 __init__ 中建立了一個 layout,裡面塞一些 QWidget,這種 layout 因為用完就可以丟了,所以就不需要self,只要寫layout = QtGui.QVBoxLayout(),不用寫成 self.layout = QtGui.QVBoxLayout()

然而,假設有一個 QLabel,在 __init__ 裡面定義過後,你想要再透過其他 method 來存取他(例如定義一個 method updateLabelText(self, string) ,藉由這個 method 來修改該 QLabel),這種時候就要加上 self,讓他能夠被「跨 method 存取」:

1
2
3
4
5
6
7
class MyWidget(QtGui.QWidget):
def __init__(self, parent = None):
super(MyWidget, self).__init__(parent)
self.label = QtGui.QLabel("Hello World!") #如果這裡不使用 self,updateLabelText() 這個 method 將會無法存取該 label,也就無法順利 `setText()`
# ...以下略
def updateLabelText(self, string):
self.label.setText(string)

使用 self 前綴的東西,該 class 內的所有 methods 都能夠存取它。

parent

parent (不是 OOP 那個 parent class!很重要!)是 PyQt 裡很常用的概念,他可以讓各個 QWidget 之間有 parent & child 的從屬關係。使用時機有:

  • parent 被關閉時,child 物件也會隨之關閉。
  • 一個 QWidget A(可以是標準 QWidget 或任何從 QWidget 所 subclass 出來的物件,當然也可以是自己自訂過的 QWidget)後,想要把它作為放進 QWidget B(也就是 A 是 child,B 是 parent),並讓 B 的 instance 也能存取 A 的 instance。

怎麼用?自己在自訂 QWidget 時記得在__init__的參數加上parent=None,以及super()時也記得要加上 parent,像是這樣:super().__init__(parent)

1
2
3
4
5
6
class MyWidget(QtGui.QWidget):
def __init__(self, parent = None):
super(MyWidget, self).__init__(parent)
self.label = QtGui.QLabel("Hello World!")
self.line_edit = QtGui.QLineEdit()
# ...以下略

往後要建立 MyWidget 的 instance 時,記得以self作為 MyWidget 的參數

1
self.my_widget = MyWidget(self)

現在,這個 self 就是 self.my_widget 的 parent 囉。

至於我要怎麼在 MyWidget 的定義中存取 parent 呢?很簡單,self.parentWidget() 就是 parent 了!


[2014-09-22 月 22:31] 新增 parent 部份。