本文索引:
咱们在显示一些模态对话框的时候,每每须要将对话框的背景颜色调暗以达到突出当前对话框的效果,例如:
对话框的父窗口除了标题栏之外的部分都变暗了,在父窗口的对比下对话框的显示效果就获得了强调。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了,父窗口的事件循环会将屡次绘制事件智能的合并,因此遮罩可能根本不会被显示出来,所以咱们必须在对话框前显示遮罩。(若是你好奇的话能够把两行代码的顺序对调,看看是否能正常显示遮罩控件)
这样咱们的遮罩控件就完成了,运行程序: