本文索引
- 需求
- 原理
- 實作遮罩控件
- 遮罩的使用
我們在顯示一些模态對話框的時候,往往需要将對話框的背景顔色調暗以達到突出目前對話框的效果,例如:

對話框的父視窗除了标題欄以外的部分都變暗了,在父視窗的對比下對話框的顯示效果就得到了強調。
這種設計多見于web頁面,當使用者點選諸如購買之類的按鈕後頁面會彈出一個購物清單确認對話框,并将對話框以外的内容用類似圖中的效果處理,使使用者可以将注意力集中在對話框本身。
今天我們也将使用Qt來實作這一效果。
在介紹具體做法前我想先介紹一點預備知識——“亮盒效果”。這是一個攝影技術的名詞,大意是指将背景暗化以便突出照片的主體,因為往往使用一個黑色的“盒子”來罩住需要拍攝的主體,是以被稱為亮盒。而這與我們想實作的效果不謀而合。
是以想要實作讓對話框的父視窗變暗的效果,最常見的手段就是使用一個半透明遮罩控件将父視窗元件整個遮住。
可能有人會問,既然隻需要将背景暗化,那為何不直接修改父視窗的QSS,而要使用一個遮罩元件呢?原因也很簡單,因為父控件的
background
屬性是少數幾個能被子控件繼承的屬性,當我們修改了父視窗的QSS那麼我們的對話框也将不可避免的遭受影響,雖然可以使用
setStyleSheet('')
去除這些額外的影響,但是這樣做将會引入許多不必要的複雜性,顯然是與我們的設計初衷相違背的。
是以我們選擇使用遮罩控件。回顧一下
QWidget
的特性,當除了
QDialog
以外的控件設定了非
None
的parent時,該控件就會繪制在parent控件上。布局管理器隻是幫助我們設定了parent并自動指定了一個合适的位置和尺寸來繪制控件,是以我們完全可以自己指定控件的大小和需要繪制的區域。
繪制區域使用的是
QWidget
的邏輯坐标。與painter使用的坐标系統一緻。是以我們隻需要設定遮罩元件的parent為父視窗,然後擷取父視窗的高度和寬度,并設定遮罩元件的大小與父視窗一緻,最後從父視窗邏輯坐标系的
(0, 0)
出開始繪制控件即可保證遮罩控件可以完整的遮蓋住父視窗實作遮罩效果。
注意,如果子控件的繪制區域或者大小超過了父控件,超過的部分将會被截斷,也就是說不會顯示出來。不過不用擔心,Qt為我們提供了
geometry
和
setGeometry
接口,通過它們就可以友善的控制widgets的形狀和位置而不用擔心出錯。
下面就讓我們看一下python3實作的遮罩控件。
先看代碼:
class MaskWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlag(Qt.FramelessWindowHint, True)
self.setAttribute(Qt.WA_StyledBackground)
self.setStyleSheet('background:rgba(0,0,0,102);')
self.setAttribute(Qt.WA_DeleteOnClose)
def show(self):
"""重寫show,設定遮罩大小與parent一緻
"""
if self.parent() is None:
return
parent_rect = self.parent().geometry()
self.setGeometry(0, 0, parent_rect.width(), parent_rect.height())
super().show()
遮罩控件的實作相當簡單,隻需要注意一些細節。
遮罩控件的初始化和普通的自定義控件的過程一樣,不過需要注意的是
self.setAttribute(Qt.WA_StyledBackground)
這一行,自定義控件隻有設定該屬性後才能正常設定背景。
随後我們還設定了無邊框視窗和deleteOnClose,遮罩不需要顯示任何邊框,不過這裡的deleteOnClose可以不用設定,因為python使用的pyqt可以完美地配合gc,當控件不在被使用時可以自動釋放資源,不過我還是養成了顯示釋放的習慣,明确對資源的處理永遠都不是壞事。
第一個重點在于那句QSS。QSS中也可以設定rgba顔色,不過與css相比有一些差別。最後的alpha參數,css中通常是0-1的實數或者一個百分數,而在QSS中它是一個0-255的整數值,而我們想要實作半透明的黑色遮罩,就需要指定控件背景色透明度為40%,也就是
255 * 0.4 = 102
,最終的結果就是
rgba(255, 0, 0, 102)
,設定完成後控件就擁有了半透明效果。
第二個重點在重寫的
show
方法上。光設定了顔色和透明度還不夠,我們還要讓控件正确地遮蓋住parent。為了達到這一目的,我們先擷取parent的geometry,然後使用
self.setGeometry(0, 0, parent_rect.width(), parent_rect.height())
将控件設定到與parent重合(原理參考上一節内容)。而如果我們沒有給控件設定parent,那麼控件什麼也不會做,因為控件本身需要依賴于parent,如果沒有的話也就沒法正常顯示了。之後再使用
QWidget.show()
就可以顯示我們的遮罩效果了。
使用遮罩也相當簡單:
class MyWidget(QWidget):
"""測試遮罩的顯示效果
"""
def __init__(self):
super().__init__()
# 設定白色背景,友善顯示出遮罩
self.setStyleSheet('background:white;')
main_layout = QVBoxLayout()
button = QPushButton('點選顯示對話框')
button.clicked.connect(self.show_dialog)
main_layout.addStretch(5)
main_layout.addWidget(button, 1, Qt.AlignCenter)
self.setLayout(main_layout)
self.show()
def show_dialog(self):
dialog = QDialog(self)
dialog.setModal(True)
dialog_layout = QVBoxLayout()
dialog_layout.addWidget(QLabel('<font color="red">mask test</font>'))
dialog.setLayout(dialog_layout)
mask = MaskWidget(self)
mask.show()
dialog.exec()
mask.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()
遮罩的使用分為如下個步驟:
- 根據需要遮蓋的控件建立MaskWidget
- 顯示遮罩
- 在模态對話框關閉後調用
清除遮罩close()
之是以要在對話框顯示之前先顯示遮罩,是因為顯示模态對話框後父視窗的事件循環被阻塞,這時所有對父視窗的操作都是被阻塞的,而對話框關閉後遮罩就被close了,父視窗的事件循環會将多次繪制事件智能的合并,是以遮罩可能根本不會被顯示出來,是以我們必須在對話框前顯示遮罩。(如果你好奇的話可以把兩行代碼的順序對調,看看是否能正常顯示遮罩控件)
這樣我們的遮罩控件就完成了,運作程式:
