PyQt5入門教程


0x00 安裝環境清單

我使用的環境如下:

  • Windows 10 (Build 17763)
  • Python 3.7.2
  • VSCode 1.33.0
  • PyQt5
  • Qt Designer

如果你使用的是OSX或者Linux,請自行替換教程中的一些操作。

本文並不討論Python和VSCode的安裝,如果沒有VSCode,你可以用各種同類IDE替代或者安裝它。

本文不討論多Python共存,畢竟Python2.7在2020年就要退役了,而且我本人也沒這需求。

0x01 安裝PyQt5

下面直接使用pip來安裝PyQt5,此處可能是pip/pip3,或者兩者皆可,后面不再重復

直接pip安裝PyQt5

pip install PyQt5
  • 1

由於Qt Designer已經在Python3.5版本從PyQt5轉移到了tools,因此我們還需要安裝pyqt5-tools

pip install pyqt5-tools
  • 1

到這一步,PyQt5就安裝完成了,你可以通過下面若干可選的操作來檢查是否已經安裝成功:

  • Win+S呼出Cornata主面板(搜索框),輸入designer,如果看到跟下圖類似的結果說明PyQt Designer已經被安裝

designer_install

  • 在cmd中輸入pyuic5,如果返回“Error: one input ui-file must be specified”說明安裝成功。

0x02 初識Qt Designer

注:Qt Designer的界面是全英文的,幸運的是有漢化方法,不過因為我本人用不上,所以如果有這方面需求可以自行搜索。

我比較習慣用Win+S呼出Cornata主面板(搜索框)來啟動各種應用,那么這里就是在搜索框中輸入designer並敲回車,就能夠啟動Qt Designer了。

初次啟動會彈出這個“New Form”窗口,一般來說選擇“Main Window”然后點擊“Create”就可以了。下方有個“Show this Dialogue on Startup”的checkbox,如果不想每次啟動都看到這個“New Form”窗口,可以取消勾選。

new_form
創建“Main Window”之后,我們會看到如下畫面

designer_ui
下面就來簡單介紹下整個畫面的構成:

  • 左側的“Widget Box”就是各種可以自由拖動的組件

  • 中間的“MainWindow - untitled”窗體就是畫布

  • 右上方的"Object Inspector"可以查看當前ui的結構

  • 右側中部的"Property Editor"可以設置當前選中組件的屬性

  • 右下方的"Resource Browser"可以添加各種素材,比如圖片,背景等等,目前可以不管

大致了解了每個板塊之后,就可以正式開始編寫第一個UI了

0x03 HelloWorld!

注:從這里開始,相關代碼可以在/assets/code/pyqt5中找到

注:本文用到的代碼都在我github,就不在CSDN這里上傳了

通常來說,編寫GUI有兩種方法:第一種就是直接使用方便快捷的Qt Designer,第二種就是寫代碼。在有Qt Designer的情況下,是完全不推薦費時費力去手寫GUI代碼的。Qt Designer可以所見即所得,並且可以方便的修改並做出各種調整。

按照慣例,我們先來實現一個能夠顯示HelloWorld的窗口。

1)添加文本

在左側的“Widget Box”欄目中找到“Display Widgets”分類,將“Label”拖拽到屏幕中間的“MainWindow”畫布上,你就獲得了一個僅用於顯示文字的文本框,如下圖所示。

designer_create_label

2)編輯文本

雙擊上圖中的“TextLabel”,就可以對文本進行編輯,這里我們將其改成“HelloWorld!”,如下圖所示。如果文字沒有完全展示出來,可以自行拖拽空間改變尺寸。

特別提醒,編輯完文本之后記得敲擊回車令其生效!

designer_change_label_text

3)添加按鈕

使用同樣的方法添加一個按鈕(PushButton)並將其顯示的文本改成“HelloWorld!”,如下圖所示。

designer_create_pushbutton

4)修改窗口標題

下面修改窗口標題。選中右上方的"Object Inspector"中的“MainWindow”,然后在右側中部的"Property Editor"中找到“windowTitle”這個屬性,在Value這一欄進行修改,修改完記得敲擊回車。

5)編輯菜單欄

注意到畫布的左上方有個“Type Here”,雙擊它即可開始編輯菜單欄。菜單欄支持創建多級菜單以及分割線(separator)。我隨意創建了一些菜單項目,如下圖所示。

designer_create_menu

6)預覽

使用快捷鍵Ctrl+R預覽當前編寫的GUI(或者從菜單欄的Form > Preview / Preview in進入)

designer_preview

7)保存

如果覺得完成了,那就可以保存成*.ui的文件,這里我們保存為HelloWorld.ui。為了方便演示,我將文件保存到D盤。

8)生成Python代碼

使用cmd將目錄切到D盤並執行下面的命令。請自行將下面命令中的name替換成文件名,比如本例中的“HelloWorld.ui”

pyuic5 -o name.py name.ui
  • 1

生成的代碼應該類似下圖所示

designer_code_helloworld

9)運行Python代碼

此時嘗試運行剛剛生成的“HelloWorld.py”是沒用的,因為生成的文件並沒有程序入口。因此我們在同一個目錄下另外創建一個程序叫做“main.py”,並輸入如下內容。在本例中,gui_file_name就是HelloWorld,請自行替換。

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow

import gui_file_name

if __name__ == '__main__':
    app = QApplication(sys.argv)
    MainWindow = QMainWindow()
    ui = gui_file_name.Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然后運行“main.py”,你就能看到剛剛編寫的GUI了!

designer_run_helloworld

10)組件自適應

如果你剛剛嘗試去縮放窗口,會發現組件並不會自適應縮放,因此我們需要回到Qt Designer中進行一些額外的設置。

點擊畫布空白處,然后在上方工具欄找到grid layout或者form layout,在本例中我們使用grid layout。兩種layout的圖標如下圖所示。

designer_layout_helloworld

順帶一提,上圖中layout的左邊有三條橫線以及三條豎線的圖標,這兩個是用於對齊組件,非常實用。

設置grid layout后,我們使用Ctrl+R預覽,這次組件可以自適應了!因為我們已經將UI(HelloWorld.py/HelloWorld.ui)跟邏輯(main.py)分離,因此直接重復步驟7-8即可完成UI的更新,無需改動邏輯(main.py)部分。

0x04 Interaction

剛剛寫的HelloWorld中,我們設置的按鈕(PushButton)是沒有實際作用的,因為我們並沒有告訴這個按鈕應該做什么。實際上,要讓這個按鈕做點什么只需要增加一行代碼就可以了。

1)獲取按鈕id

打開HelloWorld.ui,在designer中選中對應的按鈕,從“Property Editor”中可以得知這個按鈕的“objectName”叫做“pushButton”,如下圖所示。

designer_pushbutton_id

2)設置觸發

Qt中有“信號和槽(signal and slot)”這個概念,不過目前無需深究,也無需在Designer中去設置對應按鈕的“信號和槽”,直接在“main.py”中“MainWindow.show()”的后面加入下面這樣的一行代碼

ui.pushButton.clicked.connect(click_success)
  • 1

下面簡單解釋下這行代碼

  • pushButton就是剛剛獲取的按鈕id
  • clicked就是信號,因為是點擊,所以我們這里用clicked
  • click_success就是對應要調用的槽,注意這里函數並不寫成click_success()

3)設置函數

既然剛剛設置了按鈕的觸發並綁定了一個函數click_success,我們就要在“main.py”中實現它。示例如下

def click_success():
    print("啊哈哈哈我終於成功了!")
  • 1
  • 2

4)運行!

UI跟邏輯分離的好處就在這里,我們這次不用去管“HelloWorld.py”了,直接運行修改完的“main.py”。點擊按鈕,這次你會發現在控制台中有了我們預設的輸出。

0x05 Conversion

這次我們來進行實戰演練,編寫一個帶GUI的匯率轉換器。

1)設計UI

conversion_ui

通過上面的講解,你應該能夠毫無壓力的設計上面這樣的UI並獲得對應的代碼。如果不行,那么不建議繼續往下閱讀,應當回頭復習。

2)傳參

現在我們有了GUI的代碼以及上一節中使用的“main.py”,我們可以開始編寫這個匯率轉換器的邏輯部分。

在上一節,我們介紹了如何讓按鈕響應點擊操作,但是並沒有接受任何參數,而且只是在控制台輸出。但是,上一節中說明了並不能通過正常的方式進行傳參。因此,對於傳參,有兩種解決方案,一種是使用lambda,還有一種是使用functool.partial。在接下來的環節中我們會使用partial。

partial的用法如下所示:

partial(function, arg1, arg2, ......)
  • 1

既然使用partial傳參,那么我們就要在程序(main.py)的頭部加上下面這行。

from functools import partial
  • 1

然后我們把上一節中的按鈕觸發那行代碼修改成下面這樣。

ui.pushButton.clicked.connect(partial(convert, ui))
  • 1

3)編寫convert函數

首先,我們要獲取用戶輸入的數字。為了使得教程簡潔易懂,我們這次只講解單向的匯率轉換。既然是單項的轉換,那么我們只需要獲取左側的文本框id。在本例中,左側的文本框id為lineEdit。如果你對此感到一頭霧水,請停下並回頭復習。

獲取文本使用的是text()方法,因此讀取用戶輸入的代碼如下

input = ui.lineEdit.text()
  • 1

接着我們進行匯率轉換,注意這里要進行類型轉換

result = float(input) * 6.7
  • 1

最后我們讓右邊的文本框顯示結果

ui.lineEdit_2.setText(str(result))
  • 1

下面是convert函數的代碼

def convert(ui):
    input = ui.lineEdit.text()
    result = float(input) * 6.7
    ui.lineEdit_2.setText(str(result))
  • 1
  • 2
  • 3
  • 4

一個簡單的匯率轉換器就這樣誕生了!

那么,如何知道一個組件都有什么方法呢?直接去Qt官方文檔查看就可以了。本節使用到的lineEdit的相關方法在這里

0x06 threading

1)前言

這幾天在用PyQt5寫東西的時候遇到這樣一個問題,網上資料也特別少,我感覺值得拿出來說一說。

我的程序中使用了threading模塊,GUI作為主線程去啟動負責邏輯處理的子線程。其中,我設計的GUI里頭有一個日志框,用來代替終端顯示各種日志輸出。既然子線程是負責邏輯處理,那么想當然的就會直接在子線程操作GUI的顯示。

都說了想當然,那當然不行咯,在子線程對GUI操作的時候,終端會出現下面這個錯誤,但是程序又不會馬上閃退。

QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
  • 1
  • 2

更讓人摸不着頭腦的是,過一陣子閃退的時候,會出現下面這句話:

段錯誤,核心已轉儲
  • 1

這啥玩意兒?能說人話么?一番搜索之后,發現這個原來英語叫做“Segmentation fault (core dumped)”。

"Segmentation fault"用人話來說大概就是“你嘗試訪問你無法訪問的內存”。

然后我把上面的報錯信息搜索了下,發現之前有人在StackOverflow問過,但是答案牛頭不對馬嘴,不過倒是在評論區發現了大佬的留言。

It is likely that the asker was not actually directly using QTextCursor, but rather using GUI code from a thread that was not the GUI thread. Attempting this seems to result in this error arising from Qt-internal code, e.g. for QTextEdit.append()
  • 1

簡而言之,就是說雖然報錯顯示QTextCursor,但是實際上是在其它線程通過Qt內部的方法間接調用了這個東西。

熱心大佬還留了個鏈接,我跟過去看了,收獲不少。

It appears you're trying to access QtGui classes from a thread other than the main thread. Like in some other GUI toolkits (e.g. Java Swing), that's not allowed.

Although QObject is reentrant, the GUI classes, notably QWidget and all its subclasses, are not reentrant. They can only be used from the main thread.
  • 1
  • 2
  • 3

這個終於說到點子上了,一句話總結就是子線程不能調用主線程的QtGui類。

所以大佬給出的方案如下:

A solution is to use signals and slots for communication between the main thread (where the GUI objects live) and your secondary thread(s). Basically, you emit signals in one thread that get delivered to the QObjects via the other thread.
  • 1

大概翻譯下,就是說可以通過信號和槽來完成子線程跟GUI所在的主線程的通信,就是通過在子線程釋放信號,傳遞到主線程的槽來完成。

可惜的是,大佬並沒有給出示例代碼,那接下來就是動手實踐了。

2)實踐

首先我們在子線程的代碼中創建一個對象,並且繼承QObject(因為需要釋放信號)。

class UpdateLog(QObject):
    update_signal = pyqtSignal()
 
    def __init__(self):
        QObject.__init__(self)
 
    def update(self):
        self.update_signal.emit()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

update_signal = pyqtSignal()就是使用Signal類來創建一個自定義的信號。

self.update_signal.emit()就是當條件滿足的時候,子線程可以調用UpdateLog類的update方法,就會發出信號。

做完這些之后,主線程中別忘了連擊信號和槽,比如self.afk.utils.logger.update_signal.connect(self.write_log)。然后現在再嘗試運行程序,就沒有任何問題了。

不僅如此,其實其它需要共享的信息,也可以通過自定義信號和槽來傳遞。

那么,現在就可以愉快的在PyQt程序中使用threading模塊了。

0x0? 小結

本文只是拋磚引玉,上面這些只是PyQt5的入門內容。不過學會了簡單的交互方法,其它的也差不多能依葫蘆畫瓢做出來。

本文中設計的程序在/assets/code/pyqt5中。

那么,就先寫到這里了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM