本文索引:
咱們在顯示一些模態對話框的時候,每每須要將對話框的背景顏色調暗以達到突出當前對話框的效果,例如:
對話框的父窗口除了標題欄之外的部分都變暗了,在父窗口的對比下對話框的顯示效果就獲得了強調。css
這種設計多見於web頁面,當用戶點擊諸如購買之類的按鈕後頁面會彈出一個購物清單確認對話框,並將對話框之外的內容用相似圖中的效果處理,使用戶能夠將注意力集中在對話框自己。python
今天咱們也將使用Qt來實現這一效果。web
在介紹具體作法前我想先介紹一點預備知識——「亮盒效果」。這是一個攝影技術的名詞,大意是指將背景暗化以便突出照片的主體,由於每每使用一個黑色的「盒子」來罩住須要拍攝的主體,因此被稱爲亮盒。而這與咱們想實現的效果不謀而合。app
因此想要實現讓對話框的父窗口變暗的效果,最多見的手段就是使用一個半透明遮罩控件將父窗口組件整個遮住。less
可能有人會問,既然只須要將背景暗化,那爲什麼不直接修改父窗口的QSS,而要使用一個遮罩組件呢?緣由也很簡單,由於父控件的background
屬性是少數幾個能被子控件繼承的屬性,當咱們修改了父窗口的QSS那麼咱們的對話框也將不可避免的遭受影響,雖然可使用setStyleSheet('')
去除這些額外的影響,可是這樣作將會引入許多沒必要要的複雜性,顯然是與咱們的設計初衷相違背的。佈局
因此咱們選擇使用遮罩控件。回顧一下QWidget
的特性,當除了QDialog
之外的控件設置了非None
的parent時,該控件就會繪製在parent控件上。佈局管理器只是幫助咱們設置了parent並自動指定了一個合適的位置和尺寸來繪製控件,因此咱們徹底能夠本身指定控件的大小和須要繪製的區域。測試
繪製區域使用的是QWidget
的邏輯座標。與painter使用的座標系統一致。因此咱們只須要設置遮罩組件的parent爲父窗口,而後獲取父窗口的高度和寬度,並設置遮罩組件的大小與父窗口一致,最後從父窗口邏輯座標系的(0, 0)出開始繪製控件便可保證遮罩控件能夠完整的遮蓋住父窗口實現遮罩效果。ui
注意,若是子控件的繪製區域或者大小超過了父控件,超過的部分將會被截斷,也就是說不會顯示出來。不過不用擔憂,Qt爲咱們提供了geometry
和setGeometry
接口,經過它們就能夠方便的控制widgets的形狀和位置而不用擔憂出錯。設計
下面就讓咱們看一下python3實現的遮罩控件。code
先看代碼:
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_()
遮罩的使用分爲以下個步驟:
close()
清除遮罩之因此要在對話框顯示以前先顯示遮罩,是由於顯示模態對話框後父窗口的事件循環被阻塞,這時全部對父窗口的操做都是被阻塞的,而對話框關閉後遮罩就被close了,父窗口的事件循環會將屢次繪製事件智能的合併,因此遮罩可能根本不會被顯示出來,所以咱們必須在對話框前顯示遮罩。(若是你好奇的話能夠把兩行代碼的順序對調,看看是否能正常顯示遮罩控件)
這樣咱們的遮罩控件就完成了,運行程序: