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