到目前為止,我們已經(jīng)創(chuàng)建了一個(gè)窗口,并向其中添加了一個(gè)簡(jiǎn)單的按鈕控件,但按鈕并不做任何事情。這一點(diǎn)用都沒有——當(dāng)您創(chuàng)建GUI應(yīng)用程序時(shí),您通常希望它們做一些事情!我們需要的是一種將按下按鈕的行為與發(fā)生某事聯(lián)系起來的方法。在Qt中,這是由信號(hào)和槽或事件提供的。 Signals & Slots信號(hào)是控件在發(fā)生某些事情時(shí)發(fā)出的通知。這個(gè)東西可以是任何數(shù)量的事情,從按下一個(gè)按鈕,到輸入框的文本更改,到窗口的文本更改。許多信號(hào)是由用戶操作發(fā)起的,但這不是一條規(guī)則。 除了通知正在發(fā)生的事情之外,信號(hào)還可以發(fā)送數(shù)據(jù),以提供關(guān)于所發(fā)生事情的附加上下文。 您還可以創(chuàng)建自己的自定義信號(hào),我們將在后面討論。 槽是Qt用于信號(hào)接收器的名稱。在Python中,應(yīng)用程序中的任何函數(shù)(或方法)都可以用作槽——只需將信號(hào)連接到它即可。如果信號(hào)發(fā)送數(shù)據(jù),那么接收函數(shù)也將接收該數(shù)據(jù)。許多Qt控件也有自己的內(nèi)置槽,這意味著您可以直接將Qt控件掛鉤在一起。 讓我們來看看Qt信號(hào)的基礎(chǔ)知識(shí),以及如何使用它們來連接控件,使應(yīng)用程序中發(fā)生一些事情。 將以下應(yīng)用程序大綱保存到名為app.py的文件中。 import sysfrom PyQt6.QtWidgets import QApplication, QMainWindow, QPushButtonclass MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('My App')app = QApplication(sys.argv)window = MainWindow()window.show()app.exec() QPushButton信號(hào)我們的簡(jiǎn)單應(yīng)用程序目前有一個(gè)設(shè)置為中心控件的QMainWindow和QPushButton。讓我們從將這個(gè)按鈕連接到一個(gè)自定義Python方法開始。在這里,我們創(chuàng)建了一個(gè)名為the_button_was_clicked的簡(jiǎn)單自定義槽,它接受來自QPushButton的單擊信號(hào)。
運(yùn)行它!如果您單擊按鈕,您將在控制臺(tái)上看到文本“Clicked!”。 Clicked!Clicked!Clicked!Clicked! 接收數(shù)據(jù)這是一個(gè)好的開始!我們已經(jīng)聽說,信號(hào)也可以發(fā)送數(shù)據(jù),以提供關(guān)于剛剛發(fā)生的事情的更多信息。.clicked信號(hào)也不例外,它也為按鈕提供了已檢查(或已切換)的狀態(tài)。對(duì)于普通按鈕,這總是False,所以我們的第一個(gè)槽忽略了這個(gè)數(shù)據(jù)。然而,我們可以使我們的按鈕可檢查,并查看效果。 在下面的示例中,我們添加第二個(gè)槽,用于輸出checkstate。
運(yùn)行它!如果你按下這個(gè)按鈕,你會(huì)看到它被高亮顯示為選中。再次按下釋放。在控制臺(tái)中查找檢查狀態(tài)。 Clicked!Checked? TrueClicked!Checked? FalseClicked!Checked? TrueClicked!Checked? FalseClicked!Checked? True 您可以將任意多個(gè)槽連接到一個(gè)信號(hào),并且可以在槽上同時(shí)響應(yīng)不同版本的信號(hào)。 存儲(chǔ)數(shù)據(jù)通常,將控件的當(dāng)前狀態(tài)存儲(chǔ)在Python變量中是很有用的。這允許您像使用任何其他Python變量一樣使用這些值,而無(wú)需訪問原始的控件。您可以將這些值存儲(chǔ)為單獨(dú)的變量,也可以使用字典(如果愿意的話)。在下一個(gè)例子中,我們將按鈕的檢查值存儲(chǔ)在self上名為button_is_checked的變量中。
首先,我們?yōu)樽兞吭O(shè)置默認(rèn)值(為True),然后使用默認(rèn)值設(shè)置控件的初始狀態(tài)。當(dāng)控件狀態(tài)改變時(shí),我們接收信號(hào)并更新變量以匹配。 您可以對(duì)任何PyQt控件使用相同的模式。如果控件沒有提供發(fā)送當(dāng)前狀態(tài)的信號(hào),則需要直接在處理程序中從控件檢索值。例如,這里我們正在檢查pressed處理程序中的checked狀態(tài)。 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.button_is_checked = True self.setWindowTitle('My App') self.button = QPushButton('Press Me!') self.button.setCheckable(True) self.button.released.connect(self.the_button_was_released) self.button.setChecked(self.button_is_checked) self.setCentralWidget(self.button) def the_button_was_released(self): self.button_is_checked = self.button.isChecked() print(self.button_is_checked) 我們需要在self上保持對(duì)按鈕的引用,這樣我們才能在槽中訪問它。 released信號(hào)在按鈕釋放時(shí)觸發(fā),但不發(fā)送check狀態(tài),因此我們使用.ischecked()從處理程序中的按鈕獲取check狀態(tài)。 改變接口到目前為止,我們已經(jīng)了解了如何接受信號(hào)并將輸出打印到控制臺(tái)。但是當(dāng)我們點(diǎn)擊按鈕時(shí),如何讓界面發(fā)生一些事情呢?讓我們更新我們的slot方法來修改按鈕,更改文本并禁用按鈕,使其不再可點(diǎn)擊。我們還將暫時(shí)關(guān)閉可檢查狀態(tài)。
同樣,因?yàn)槲覀冃枰軌蛟L問the_button_was_clicked方法中的按鈕,所以我們?cè)趕elf中保留了對(duì)它的引用。通過向.setText()傳遞一個(gè)str來更改按鈕的文本。使用False禁用按鈕調(diào)用.setEnabled()。 運(yùn)行它!如果你點(diǎn)擊按鈕,文本將會(huì)改變,按鈕將變得不可點(diǎn)擊。 你不局限于改變觸發(fā)信號(hào)的按鈕,你可以在你的slot方法中做任何你想做的事情。例如,嘗試向button_was_clicked方法添加以下代碼行,以更改窗口標(biāo)題。 self.setWindowTitle('A new window title') 大多數(shù)控件都有自己的信號(hào),我們正在使用的QMainWindow也不例外。在下面這個(gè)更復(fù)雜的例子中,我們將QMainWindow上的.windowTitleChanged信號(hào)連接到一個(gè)自定義的slot方法。 在下面的例子中,我們將QMainWindow上的.windowTitleChanged信號(hào)連接到方法槽the_window_title_changed上。該槽還接收新的窗口標(biāo)題。
首先,我們?cè)O(shè)置了一個(gè)窗口標(biāo)題列表——我們將使用Python內(nèi)置的random.choice()從這個(gè)列表中隨機(jī)選擇一個(gè)。我們將自定義槽方法the_window_title_changed與窗口的.windowTitleChanged信號(hào)掛鉤。 當(dāng)我們點(diǎn)擊按鈕時(shí),窗口標(biāo)題將隨機(jī)變化。如果新的窗口標(biāo)題等于“Something went wrong”按鈕將被禁用。 在這個(gè)例子中有幾點(diǎn)需要注意。 首先,在設(shè)置窗口標(biāo)題時(shí)并不總是發(fā)出windowTitleChanged信號(hào)。只有在新標(biāo)題與前一個(gè)標(biāo)題發(fā)生變化時(shí),該信號(hào)才會(huì)觸發(fā)。如果多次設(shè)置相同的標(biāo)題,則只會(huì)在第一次觸發(fā)信號(hào)。重要的是要反復(fù)檢查信號(hào)觸發(fā)的條件,以避免在應(yīng)用中使用它們時(shí)感到驚訝。 其次,注意我們是如何使用信號(hào)將事物連接在一起的。發(fā)生的一件事——按下一個(gè)按鈕——可以觸發(fā)多個(gè)其他事情依次發(fā)生。這些后續(xù)影響不需要知道是什么引起的,而只是簡(jiǎn)單規(guī)則的結(jié)果。這種效果與觸發(fā)器的解耦是構(gòu)建GUI應(yīng)用程序時(shí)要理解的關(guān)鍵概念之一。我們將在整本書中不斷地回到這一點(diǎn)! 在本節(jié)中,我們介紹了信號(hào)和槽。我們已經(jīng)演示了一些簡(jiǎn)單的信號(hào),以及如何使用它們?cè)趹?yīng)用程序周圍傳遞數(shù)據(jù)和狀態(tài)。接下來,我們將看看Qt提供的用于應(yīng)用程序中的控件——以及它們提供的信號(hào)。 將控件直接連接在一起到目前為止,我們已經(jīng)看到了連接控件信號(hào)到Python方法的例子。當(dāng)從控件觸發(fā)信號(hào)時(shí),我們的Python方法將被調(diào)用并從該信號(hào)接收數(shù)據(jù)。但您并不總是需要使用Python函數(shù)來處理信號(hào)——您也可以將Qt控件直接連接到另一個(gè)控件。 在下面的示例中,我們向窗口添加一個(gè)QLineEdit控件和一個(gè)QLabel。在窗口的__init__中,我們將QLineEdit的.textChanged信號(hào)連接到QLabel上的.setText方法?,F(xiàn)在,任何時(shí)候文本在QLineEdit中改變QLabel將接收該文本到它的.setText方法。 from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QVBoxLayout, QWidgetimport sysclass MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('My App') self.label = QLabel() self.input = QLineEdit() self.input.textChanged.connect(self.label.setText) layout = QVBoxLayout() layout.addWidget(self.input) layout.addWidget(self.label) container = QWidget() container.setLayout(layout) # Set the central widget of the Window. self.setCentralWidget(container)app = QApplication(sys.argv)window = MainWindow()window.show()app.exec() 注意,為了將輸入連接到標(biāo)簽,必須同時(shí)定義輸入和標(biāo)簽。這段代碼將兩個(gè)控件添加到一個(gè)布局中,并在窗口上設(shè)置該布局。我們稍后將詳細(xì)介紹布局,您可以先忽略它。 運(yùn)行它!在上面的框中輸入一些文本,你就會(huì)看到它立即出現(xiàn)在標(biāo)簽上。 大多數(shù)Qt控件都有可用的槽,您可以將發(fā)出它所接受的相同類型的任何信號(hào)連接到槽??丶臋n在“公共槽”下列出了每個(gè)控件的槽。例如 事件用戶與Qt應(yīng)用程序的每次交互都是一個(gè)事件。有許多類型的事件,每一種表示不同類型的交互。Qt使用事件對(duì)象表示這些事件,這些事件對(duì)象將所發(fā)生事件的信息打包。這些事件被傳遞到發(fā)生交互的控件上的特定事件處理程序。 通過定義自定義或擴(kuò)展事件處理程序,您可以更改控件響應(yīng)這些事件的方式。事件處理程序的定義與任何其他方法一樣,但名稱是特定于它們處理的事件類型的。 控件接收的主要事件之一是QMouseEvent。QMouseEvent事件是為控件上的每次鼠標(biāo)移動(dòng)和按鈕單擊創(chuàng)建的。以下事件處理程序可用于處理鼠標(biāo)事件—— 例如,單擊一個(gè)控件將導(dǎo)致QMouseEvent被發(fā)送到該控件上的.mousePressEvent事件處理程序。這個(gè)處理程序可以使用事件對(duì)象來查找關(guān)于所發(fā)生事件的信息,例如是什么觸發(fā)了事件以及事件具體發(fā)生在哪里。 可以通過子類化和重寫類上的處理程序方法來攔截事件。您可以選擇過濾、修改或忽略事件,通過使用super()調(diào)用父類函數(shù)將它們傳遞給事件的正常處理程序。這些可以添加到您的主窗口類,如下所示。在每種情況下,e將接收傳入的事件。
運(yùn)行它!嘗試在窗口中移動(dòng)和單擊(并雙擊)并觀察事件出現(xiàn)。 您將注意到,只有在按下按鈕時(shí)才會(huì)注冊(cè)鼠標(biāo)移動(dòng)事件。你可以通過在窗口上調(diào)用self.setMouseTracking(True)來改變這一點(diǎn)。您可能還會(huì)注意到,當(dāng)按下按鈕時(shí),按(單擊)和雙擊事件都會(huì)觸發(fā)。只有釋放事件在按鈕被釋放時(shí)觸發(fā)。通常情況下,要注冊(cè)用戶的點(diǎn)擊,你應(yīng)該同時(shí)注意鼠標(biāo)的下落和釋放。 在事件處理程序內(nèi)部,您可以訪問事件對(duì)象。該對(duì)象包含有關(guān)事件的信息,可用于根據(jù)具體發(fā)生的情況作出不同的響應(yīng)。接下來我們將研究鼠標(biāo)事件對(duì)象。 鼠標(biāo)事件Qt中的所有鼠標(biāo)事件都是用QMouseEvent對(duì)象跟蹤的,關(guān)于事件的信息可以從下面的事件方法中讀取。
您可以在事件處理程序中使用這些方法以不同的方式響應(yīng)不同的事件,或者完全忽略它們。位置方法作為QPoint對(duì)象提供全局和本地(與控件相關(guān)的)位置信息,而按鍵是使用Qt名稱空間中的MouseButton類型報(bào)告的。 例如,下面的命令允許我們對(duì)窗口上的左、右、中單擊做出不同的響應(yīng)。 def mousePressEvent(self, e): if e.button() == Qt.MouseButton.LeftButton: # handle the left-button press in here self.label.setText('mousePressEvent LEFT') elif e.button() == Qt.MouseButton.MiddleButton: # handle the middle-button press in here. self.label.setText('mousePressEvent MIDDLE') elif e.button() == Qt.MouseButton.RightButton: # handle the right-button press in here. self.label.setText('mousePressEvent RIGHT')def mouseReleaseEvent(self, e): if e.button() == Qt.MouseButton.LeftButton: self.label.setText('mouseReleaseEvent LEFT') elif e.button() == Qt.MouseButton.MiddleButton: self.label.setText('mouseReleaseEvent MIDDLE') elif e.button() == Qt.MouseButton.RightButton: self.label.setText('mouseReleaseEvent RIGHT')def mouseDoubleClickEvent(self, e): if e.button() == Qt.MouseButton.LeftButton: self.label.setText('mouseDoubleClickEvent LEFT') elif e.button() == Qt.MouseButton.MiddleButton: self.label.setText('mouseDoubleClickEvent MIDDLE') elif e.button() == Qt.MouseButton.RightButton: self.label.setText('mouseDoubleClickEvent RIGHT') 按鍵標(biāo)識(shí)符在Qt名稱空間中定義,如下所示:
上下文菜單上下文菜單通常在右鍵單擊窗口時(shí)出現(xiàn)。Qt支持生成這些菜單,控件有一個(gè)特定的事件用來觸發(fā)它們。在下面的例子中,我們將攔截QMainWindow中的.contextMenuEvent。每當(dāng)要顯示上下文菜單時(shí),就會(huì)觸發(fā)此事件,并傳遞一個(gè)類型為QContextMenuEvent的單值event。 要攔截事件,只需用同名的新方法覆蓋對(duì)象方法。在本例中,我們可以在MainWindow子類上創(chuàng)建一個(gè)名為contextMenuEvent的方法,它將接收所有這種類型的事件。
如果運(yùn)行上面的代碼并在窗口內(nèi)右鍵單擊,您將看到出現(xiàn)一個(gè)上下文菜單。您可以在菜單操作上設(shè)置正常的.觸發(fā)槽(并重用為菜單和工具欄定義的操作)。 當(dāng)將鼠標(biāo)位置傳遞給exec函數(shù)時(shí),這個(gè)位置必須相對(duì)于定義時(shí)傳入的父位置。在本例中,我們傳遞self作為父元素,因此我們可以使用全局位置。 為了完整起見,實(shí)際上有一種基于信號(hào)的方法來創(chuàng)建上下文菜單。 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.show() self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.on_context_menu) def on_context_menu(self, pos): context = QMenu(self) context.addAction(QAction('test 1', self)) context.addAction(QAction('test 2', self)) context.addAction(QAction('test 3', self)) context.exec(self.mapToGlobal(pos)) 這完全取決于你的選擇。 事件的層次結(jié)構(gòu)在PyQt中,每個(gè)控件都是兩個(gè)不同層次結(jié)構(gòu)的一部分:Python對(duì)象層次結(jié)構(gòu)和Qt布局層次結(jié)構(gòu)。如何響應(yīng)或忽略事件會(huì)影響UI的行為。 Python繼承轉(zhuǎn)發(fā)通常,您可能希望攔截一個(gè)事件,對(duì)它做一些事情,但仍然觸發(fā)默認(rèn)的事件處理行為。如果您的對(duì)象是從標(biāo)準(zhǔn)控件繼承的,那么它可能在默認(rèn)情況下實(shí)現(xiàn)了合理的行為。您可以通過使用super()調(diào)用父實(shí)現(xiàn)來觸發(fā)此操作。 這是Python的父類,而不是PyQt的 .parent()。
事件將繼續(xù)正常運(yùn)行,但您已經(jīng)添加了一些不干涉行為。 布局(PyQt)轉(zhuǎn)發(fā)在向應(yīng)用程序添加控件時(shí),它還從布局中獲取另一個(gè)父組件??梢酝ㄟ^調(diào)用.parent()找到控件的父類。有時(shí)你手動(dòng)指定這些父級(jí),比如QMenu或QDialog,通常是自動(dòng)的。例如,當(dāng)您向主窗口添加控件時(shí),主窗口將成為控件的父窗口。 當(dāng)為用戶與UI交互創(chuàng)建事件時(shí),這些事件被傳遞到UI中最上面的控件。因此,如果您在窗口上有一個(gè)按鈕,并單擊該按鈕,該按鈕將首先接收事件。 如果第一個(gè)控件不能處理事件,或者選擇不處理,則該事件將向父控件冒泡。這種冒泡在嵌套的控件上一直持續(xù),直到事件被處理或到達(dá)主窗口。 在您自己的事件處理程序中,可以通過調(diào)用.accept()將事件標(biāo)記為已處理。 class CustomButton(QPushButton) def mousePressEvent(self, e): e.accept() 或者,您可以通過在事件對(duì)象上調(diào)用.ignore()將其標(biāo)記為未處理。在這種情況下,事件將繼續(xù)在層次結(jié)構(gòu)中向上冒泡。
如果您希望控件對(duì)事件透明,那么您可以安全地忽略已經(jīng)以某種方式實(shí)際響應(yīng)的事件。同樣,你可以選擇接受你沒有回應(yīng)的事件,以使它們沉默。 |
|