第 4 章

QWindow

本章共 6 个小节 · PySide6 Basic Tutorial
本章要点:
  1. QWindow 类的基本用法。
  2. 绘制窗口内容。
  3. 键盘与鼠标事件。
  4. 嵌套窗口。
4.1

关于 QWindow 类

一般的 Qt应用程序不使用QWindow类来创建窗口,但在需要高度自定义窗口的情况下,使用QWindow类来创建窗口会比较灵活。

QWindow是窗口对象的基类,通常需要派生出自定义的窗口类型后再进行实例化。QWindow对象可以设置父窗口,从而组成窗口树。未设置父窗口的QWindow对象将成为顶层窗口。窗口对象在实例化之后不会马上显示到屏幕上,除非调用 show或 setVisible方法。

在创建任何窗口实例之前,必须先初始化QGuiApplication实例。在窗口显示后调用exec方法启动主消息循环。在最后一个窗口被关闭后,主消息循环才会退出。

4.1.1 一个简单的窗口

本示例仅演示 QWindow 类的基本用法,所以 CustWindow类只是简单地从QWindow类派生,并未实现自定义逻辑。

from PySide6.QtGui import QWindow, QGuiApplication
#自定义窗口类
class CustWindow(QWindow):
    #构造函数
    def __init__(self, parent = None):
        #调用基类的构造函数
        super().__init__(parent)
if __name__ == "__main__":
    #先初始化应用程序对象
    app = QGuiApplication()
    #再初始化窗口对象
    window = CustWindow()
    #显示窗口
    window.show()
    #进入主消息循环
    app.exec()

在定义窗口类时,__init__ 方法可以设置一个parent参数,类型为QWindow类(兼容QWindow的子类)。parent参数用于为当前窗口实例指定父窗口,默认值为None,表示当前窗口对象是顶层窗口。

CustWindow保留窗口属性的默认值,show方法执行后会以默认大小和位置呈现到屏幕上,如图4-1所示。

图 4-1
图 4-1 空白窗口
图 4-1 空白窗口

CustWindow呈现后其背景为纯黑色,这是因为示例代码没有为窗口绘制任何内容。

4.1.2 窗口标题

调用 setTitle方法可以设置窗口的标题栏文本,随后用 title方法获取标题栏的文本,例如:

app = QGuiApplication()
wind = QWindow()
#设置标题栏文本
wind.setTitle("App")
#显示窗口
wind.show()
#打印标题栏文本
print("标题栏:"+ wind.title())
app.exec()

4.1.3 设置窗口的位置和大小

窗口位置(Position)是指窗口左上角在容器中的坐标。对于顶层窗口,它的容器是桌面;对于子窗口,容器就是父窗口。大小是指窗口的宽度和高度。

setGeometry方法可以同时设定窗口的位置和大小,签名如下:

def setGeometry(posx, posy, w, h)

其中,posx和posy是窗口左上角在容器中的坐标值,w、h分别表示窗口的宽度和高度。

要单独设置窗口的位置可以调用 setPosition方法。

def setPosition(self, posx, posy)

要单独设置宽度和高度,可以调用resize方法。

def resize(newSize: QSize)
def resize(w: int, h: int)

resize方法有两个重载:

  1. 用一个QSize对象来封装宽度和高度;
  2. 直接用两个整数值表示。

若仅设置窗口的宽度,还可以调用 setWidth 方法。同理,调用 setHeight方法可单独设置窗口的高度。

4.1.4 示例:设置窗口的位置和大小

本示例创建了两个窗口:第一个窗口调用setGeometry方法同时设置窗口大小以及它在桌面上的位置,第二个窗口只设置窗口的宽度。

#初始化应用程序对象
app = QGuiApplication()
#第一个窗口
windowA = QWindow()
#设置标题栏文本
windowA.setTitle("窗口 1")
#设置窗口的位置和大小
windowA.setGeometry(386, 400, 350, 285)
#显示窗口
windowA.show()
#第二个窗口
windowB = QWindow()
#设置标题栏文本
windowB.setTitle("窗口 2")
#只设置窗口的宽度
windowB.setWidth(620)
#显示窗口
windowB.show()
#消息循环
app.exec()

第一个窗口通过 setGeometry方法设置其在桌面上的相对坐标为(386,400),宽度为350,高度为285;第二个窗口设置了宽度为620。

4.1.5 示例:处理窗口的Resize事件

当调用 QWindow类的 resize(或 setGeometry)方法,或者用户操作修改窗口的大小后会引发QEvent.Resize 事件,使 QWindow 类的 resizeEvent 方法被调用并传递 QResizeEvent 类型的事件参数。

QWindow 的子类可以重写 resizeEvent 方法来处理 Resize 事件。QResizeEvent 事件参数提供两个方法成员可以获取程序代码所关心的数据。

  1. oldSize:窗口在调整前的大小(旧值)。
  2. size:调整后的大小,即窗口的当前大小。

在下面的示例代码中,CustWindow类重写了resizeEvent 方法,然后使用 print 函数分别打印出窗口在调整前后的大小(宽度,高度)。

class CustWindow(QWindow):
    def resizeEvent(self, arg: QResizeEvent):
        #窗口原来的大小
        old = arg.oldSize()
        #窗口现在的大小
        cur = arg.size()
        print(f'窗口从({old.width()},{old.height()})到({cur.width()},{cur.height()})')

初始化应用程序,显示窗口。

app = QGuiApplication()
win = CustWindow()
win.show()
app.exec()

在窗口呈现后,通过鼠标调整窗口的大小,控制台会打印出以下内容:

窗口从(428,299)到(429,299)
窗口从(429,299)到(430,299)
窗口从(430,299)到(432,299)
窗口从(432,299)到(434,299)
窗口从(434,299)到(435,299)
窗口从(435,299)到(436,299)
窗口从(436,299)到(437,299)
窗口从(437,299)到(438,299)
窗口从(438,299)到(439,299)
窗口从(439,299)到(439,299)
4.2

绘制窗口内容

在前面的示例中,读者已经了解到QWindow类或其子类创建空白窗口的过程—它就像一张全新的画布,上面什么东西都没有。设计窗口如同作画一样,需要调用相关API往窗口上绘制所需内容。

QPainter类是窗口绘制的核心组件,提供一些实用方法,可绘制各种常见的可视化对象。

  1. drawLine:绘制一根线段(直线段)。
  2. drawLines:绘制多根连续的线段。
  3. drawRect:绘制矩形。
  4. drawEllipse:绘制椭圆(包括正圆)。
  5. drawArc:绘制圆弧。
  6. drawChord:绘制弦。弦与圆弧的参数接近,但弦会把起点和终点连起来。
  7. drawPoint:仅绘制单个点。
  8. drawPoints:绘制多个点(点与点之间相互独立)。
  9. drawPolyline:绘制由一组坐标点连接起来的折线段。
  10. drawPolygon:绘制多边形。此方法与 drawPolyline 类似,但 drawPolygon 方法会将折线段的起点和终点连起来,形成闭合图形。
  11. drawConvexPolygon:绘制凸多边形。
  12. drawPie:绘制扇形。
  13. drawText:绘制文本。
  14. drawImage:将指定图像绘制到画布上。
  15. fillRect:填充矩形。
  16. setBrush:设置指定的画刷。QPainter对象将自动使用此画刷去填充图形内部。
  17. setPen:设置画笔。QPainter对象将使用画笔来绘制线条或文本。

QPainter 对象必须与一个QPaintDevice 对象关联,才能进行绘图操作。QPaintDevice代表一个抽象的二维空间坐标系,原点位于左上角。X轴向右延伸为正方向,Y轴向下延伸为正方向。一个QPaintDevice对象在同一时刻只能使用一个QPainter对象。

QPaintDevice 是公共基类,一般不直接使用。Qt 中已实现 QPaintDevice 的子类有 QWidget、QPaintDeviceWindow、QImage 等。

QPainter 对象一定要调用 begin 方法后才能进行绘图操作,并通过方法参数传递一个 QPaintDevice对象的引用(如果 QPaintDevice对象无效,将无法绘图)。begin方法调用后会将画刷、画笔、字体等资源重置为默认值。如果 begin方法返回 False,表示无法在指定的 QPaintDevice 上绘图。完成绘图后还要调用 end方法来释放各种被使用的资源(如画刷)。另外,可以将 QPaintDevice 对象传递给 QPainter类的构造函数,实例化后即可用于绘图,不需要调用begin方法。也就是说,QPainter对象与QPaintDevice对象有两种关联方式,任选其一即可。

#先实例化 QPainter 对象
painter = QPainter()
#再调用 begin 方法
painter.begin(paintDevice)
#---------- 第二种方案--
#直接传给QPainter的构造函数
painter = QPainter(paintDevice)

4.2.1 QBackingStore

要在 QWindow 中绘制内容,需要用到 QBackingStore类。在初始化 QPainter 对象时,通过QBackingStore.paintDevice方法可以获取 QPaintDevice 对象的引用。下面的代码在窗口上绘制一个正方形。

#创建应用程序对象
app = QGuiApplication()
#创建窗口对象
myWind = QWindow()
#设置窗口大小
myWind.resize(680, 520)
#创建 QBackingStore 对象
backStore = QBackingStore(myWind)
#设置 QBackingStore 的大小
backStore.resize(QSize(400,400))
#先显示窗口,才能看到绘制效果
myWind.show()
#绘图区域
drawRect = QRect(QPoint(0,0), backStore.size())
#开始绘图
backStore.beginPaint(drawRect)
#创建QPainter 对象
painter = QPainter()
painter.begin(backStore.paintDevice())
#设置画笔
#颜色:R=0,G=200,B=255,A=255
#粗细:2.5
#线条样式:实线
painter.setPen(QPen(QColor(0,200,255,255), 2.5, Qt.PenStyle.SolidLine))
#画矩形
painter.drawRect(80, 70, 150, 150)
painter.end()
#结束绘图
backStore.endPaint()
#将数据发送到窗口
backStore.flush(drawRect)
#进入消息循环
app.exec()

以上代码仅用于演示,绘图操作是在窗口对象外部进行的,因此无法响应窗口大小改变、重绘等事件,即正方形只绘制一次,当调整窗口大小后,已绘制的内容会丢失。QWindow和QBackingStore都有resize方法。前者用于调整窗口的大小,后者调整画布的大小。在上述代码中窗口与绘图画布的大小不一致,使得窗口上有一部分区域无效,如图4-2所示。

图 4-2
图4-2 窗口上只有部分区域可用于绘图
图4-2 窗口上只有部分区域可用于绘图

在调用 QBackingStore类的构造函数时,需要传递一个 QWindow 对象的引用,这样才能使绘制的内容显示到指定窗口上。paintDevice 方法返回QPainter 对象所需要的 QPaintDevice 引用。该引用只在调用 beginPaint和 endPaint方法之间的代码上下文中有效。因此,开发者不需要手动释放 QPaintDevice对象。绘图结束后,调用flush方法将所绘制的内容发送到窗口,否则内容不会显示。

4.2.2 示例:绘制三角形

本示例将 QBackingStore对象封装在QWindow的派生类内部,当窗口大小改变时方便重新绘制图形。具体实现步骤如下。

  1. 定义MyWindow 类,派生自 QWindow 类。
class MyWindow(QWindow):
  1. 在__init__ 方法中设置窗口的标题、大小,以及创建QBackingStore对象实例。
def __init__(self, parent=None):
    #调用基类成员
    super().__init__(parent)
    #设置窗口标题
    self.setTitle("绘制三角形")
    #设置窗口大小
    self.resize(480, 390)
    #创建QBackingStore 对象
    self._backStore = QBackingStore(self)
  1. 重写resizeEvent成员,当窗口大小被改变后,同步修改QBackingStore对象的画布大小,让其始终与窗口的大小一致。
def resizeEvent(self, arg: QResizeEvent):
    #调整QBackingStore 对象的大小
    self._backStore.resize(arg.size())
  1. 重写paintEvent成员,在窗口上绘制等腰三角形。
def paintEvent(self, arg: QPaintEvent):
    #获取绘图区域大小,一般与窗口大小相同
    paintRect = arg.rect()
    #开始绘图
    self._backStore.beginPaint(paintRect)
    #创建QPainter 对象
    with QPainter(self._backStore.paintDevice()) as painter:
        #三角形最高的顶点 Y 坐标为80,X坐标在窗口中央
        pointA = QPoint(paintRect.width() / 2, 80)
        #左边顶点的X坐标为80,Y坐标为窗口高度减去80
        pointB = QPoint(80, paintRect.height() - 80)
        #第三个顶点的X坐标为窗口宽度减去80,Y坐标为窗口高度减去80
        pointC = QPoint(paintRect.width() - 80, paintRect.height() - 80)
        #设置画布背景颜色
        painter.setBackground(QColor("blue"))
        #擦除画布内容,使画布背景重新着色(蓝色)
        painter.eraseRect(paintRect)
        #设置画笔
        painter.setPen(QPen(QColor("yellow"), 3.0))
        #设置画刷,用于填充三角形内部
        painter.setBrush(QColor("red"))
        #绘制三角形
        painter.drawPolygon([pointA, pointB, pointC])
    #结束绘图
    self._backStore.endPaint()
    #刷新数据
    self._backStore.flush(paintRect)

QPainter类存在__enter__、__exit__成员,这表明它可以用在with 语句块中,当语句块退出时会自动释放相关资源。

调用 setBackground方法设置一个用于绘制背景的画刷(上述代码中是蓝色画刷)。随后调用eraseRect方法擦除整个绘图区域的内容,QPainter对象会使用已设置的蓝色画刷重新绘制画布背景。

drawPolygon方法用于绘制多边形,并且自动连接起点和终点。将已计算好的三个顶点坐标传递给drawPolygon方法,能绘制三角形,并使用setBrush方法所设置的红色画刷填充三角形的内部。

  1. 下面的代码初始化应用程序,并把MyWindow窗口显示到屏幕上。
app = QGuiApplication()
#实例化自定义窗口
win = MyWindow()
#显示窗口
win.show()
app.exec()

运行示例代码后,窗口上会呈现蓝色背景,以及轮廓为黄色、内部为红色的等腰三角形,如图4-3所示。

图 4-3
图4-3 窗口上呈现的等腰三角形
图4-3 窗口上呈现的等腰三角形

4.2.3 QFont

在绘制文本前,可以调用 QPainter 对象的 setFont 方法设置字体。随后调用 drawText方法所绘制的文本均使用该字体。

在Qt中,可以用QFont类来表示字体信息——包括字体名称、大小、样式、加粗等。在调用QFont构造函数时所指定的字体大小使用的计量单位是点数(point,简写为pt)。常见的默认字号为12pt,约为16像素(pixel,简写为px)。如果要以像素为单位设置字体大小,请调用 setPixelSize方法。

用于设置字体参数的常见方法如下。

  1. setBold:字体是否加粗。若参数为True表示加粗,否则表示常规模式。
  2. setItalic:字体是否倾斜(斜体)。
  3. setWeight:设置字体加粗的比重,如 Thin、Light、Bold、Black等。
  4. setPixelSize:设置字体大小,单位是像素。
  5. setPointSize:设置字体大小,单位是点。
  6. setStyle:设置字体样式(风格),一般指常规显示或者斜体。
  7. setFamily:设置字体名称。
  8. setFamilies:设置多个字体名称。字体列表中的第一个元素将作为主要字体使用。
  9. setStretch:字符宽度的拉伸因子。例如,150表示字符宽度为原来的1.5倍。

4.2.4 示例:在窗口上绘制文本

本示例将实现在窗口上绘制文本“锦绣山河”。

定义 CustWindow 类,从 QWindow 派生。

class CustWindow(QWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        #创建QBackingStore 对象
        self._backStore = QBackingStore(self)
        #设置窗口大小
        self.resize(450, 360)
        #设置窗口标题
        self.setTitle("绘制文本")
    def resizeEvent(self, arg: QResizeEvent):
        #调整画布大小
        self._backStore.resize(arg.size())
    def paintEvent(self, arg: QPaintEvent):
        #要绘制的文本
        text = "锦绣山河"
        #字体
        font = QFont()
        #设置字体名称
        font.setFamily("楷体")
        #设置字体大小
        font.setPointSize(42)
        #设置为斜体
        font.setItalic(True)
        #绘制区域
        drawRect = arg.rect()
        #开始绘图
        self._backStore.beginPaint(drawRect)
        with QPainter() as painter:
            painter.begin(self._backStore.paintDevice())
            #设置背景
            painter.setBackground(QColor("purple"))
            #设置画笔
            painter.setPen("gold")
            #擦除画布
            painter.eraseRect(drawRect)
            #设置字体
            painter.setFont(font)
            #绘制文本
            painter.drawText(80, 130, text)
        #结束绘制
        self._backStore.endPaint()
        #发送数据到窗口
        self._backStore.flush(drawRect)

绘制字体使用的是 setPen方法设置的画笔,以及 setFont 方法设置的字体。drawText方法有多个重载,上述代码使用的是以下版本:

def drawText(x: int, y: int, s: str)

x、y设置文本左上角的坐标,s表示要绘制的文本。

最终效果如图4-4所示。

图 4-4
图4-4 在窗口上绘制文本
图4-4 在窗口上绘制文本
4.3

QRasterWindow

QRasterWindow 既是 QWindow 的子类,也是 QPaintDevice 的子类。这意味着,QRasterWindow 实例可以直接传递给 QPainter 对象。因此,从QRasterWindow类派生的自定义窗口不需要手动创建QBackingStore 对象,直接重写paintEvent方法即可,例如:

class MyWindow(QRasterWindow):
    def paintEvent(self, event: QPaintEvent):
        rect = event.rect()
        #创建QPainter对象
        with QPainter(self) as painter:
            #设置背景
            painter.setBackground(QColor('white'))
            #设置画笔
            painter.setPen(QPen(QColor('blue'), 3.0, Qt.PenStyle.DotLine))
            #清空画布
            painter.eraseRect(rect)
            #计算圆心
            center = QPoint(rect.width() / 2, rect.height() / 2)
            #计算半径
            r = (rect.width() / 2) - \
                50 if rect.width() < rect.height() else (rect.height() / 2) - 50
            #绘制正圆
            painter.drawEllipse(center, r, r)
if __name__ == "__main__":
    app = QGuiApplication()
    window = MyWindow()
    window.show()
    app.exec()

在上述代码中,MyWindow类从 QRasterWindow 类派生,然后重写 paintEvent 方法,当窗口需要绘制内容时自动调用。上述代码在窗口中绘制一个正圆,如图4-5所示。

图 4-5
图 4-5 在窗口上绘制正圆
图 4-5 在窗口上绘制正圆
4.4

鼠标事件

QWindow类定义了4个与鼠标事件相关的方法成员。

  1. mousePressEvent:当鼠标上有按键被按下时触发(不管按下的是哪个键都会触发)。
  2. mouseReleaseEvent:当鼠标上某个按键被释放(松开)时触发。
  3. mouseDoubleClickEvent:当鼠标上某个按键被双击时触发(不管是哪个键都会触发)。
  4. mouseMoveEvent:当鼠标在当前窗口上移动时触发。

派生类可以重写上述方法来响应鼠标事件。

4.4.1 QMouseEvent

鼠标事件的事件参数都是QMouseEvent类。通过该类,程序代码能获取与鼠标行为相关的信息。主要成员如下。

  1. position:获取鼠标指针的局部坐标(相对于当前窗口)。
  2. globalPosition:获取鼠标指针的全局坐标(相对于屏幕或虚拟桌面)。
  3. button:获取触发事件的鼠标按键,如 LeftButton(左键)、RightButton(右键)、MiddleButton(中键)。
  4. buttons:获取鼠标事件发生时,各按键的状态。返回结果由MouseButton枚举的值组合而成。如果是按下按键事件,那么buttons方法返回的结果中将包含被按下键的值;如果是鼠标移动事件,那么所有按键都处于按下状态;如果是释放按键事件,那么buttons所返回的结果中将不包含被释放的按键。

4.4.2 示例:实时记录鼠标指针的位置

本示例将实现在窗口的左上角实时显示鼠标指针的位置。其原理是重写mouseMoveEvent方法,获取鼠标指针在当前窗口中的位置,然后调用窗口的 update方法重新绘制窗口内容(触发paintEvent),把坐标信息绘制到窗口的左上角。

假设自定义的窗口类为DemoWindow,派生自QRasterWindow类。

class DemoWindow(QRasterWindow):
    def __init__(self, parent:QWindow = None):
        super().__init__(parent)
        #设置窗口标题和窗口大小
        self.setTitle("记录鼠标指针的坐标")
        self.resize(600, 480)
...

实现两个辅助方法。getFont方法创建QFont(字体)对象并返回;drawCursorPos方法接收一个QPainter对象,并使用它把坐标值绘制到窗口的左上角。

def _getFont(self) -> QFont:
    if not hasattr(self, "_font"):
        f = QFont()
        f.setPointSize(24)
        self._font = f
    return self._font
def _drawCursorPos(self, painter: QPainter):
    #获取鼠标指针的当前位置
    pos: QPointF
    if hasattr(self, "_curPos"):
        pos = self._curPos
    else:
        pos = QPointF()
    #要绘制的文本
    text = f"{pos.x():.0f}, {pos.y():.0f}"
    painter.drawText(20,40,text)

重写paintEvent方法,完成窗口内容的绘制。

def paintEvent(self, arg: QPaintEvent) -> None:
    rect = arg.rect()
    #绘图
    with QPainter(self) as painter:
        #设置背景
        painter.setBackground(QColor("black"))
        #设置画笔
        painter.setPen("white")
        #设置字体
        painter.setFont(self._getFont())
        #清除画布
        painter.eraseRect(rect)
        #调用自定义方法
        self._drawCursorPos(painter)

重写mouseMoveEvent方法,获得鼠标指针的当前位置。

def mouseMoveEvent(self, arg: QMouseEvent) -> None:
    #获得当前鼠标指针的坐标
    self._curPos = arg.position()
    #刷新窗口,重新绘制
    self.update()

实例化 DemoWindow 窗口,并显示到屏幕上。

app = QGuiApplication()
window = DemoWindow()
window.show()
app.exec()

运行结果如图4-6所示。

图 4-6
图 4-6 实时显示鼠标指针的位置
图 4-6 实时显示鼠标指针的位置

4.4.3 示例:处理鼠标按键事件

鼠标按键在事件中存在“按下”和“释放”状态。本示例依次重写mousePressEvent(按键按下)和mouseReleaseEvent(按键释放)方法,并用print 函数打印相关文本。

class MyWindow(QRasterWindow):
    def mousePressEvent(self, arg: QMouseEvent):
        btn = arg.button()
        if btn == Qt.MouseButton.LeftButton:
            print('按下了鼠标左键')
        elif btn == Qt.MouseButton.RightButton:
            print('按下了鼠标右键')
        elif btn == Qt.MouseButton.MiddleButton:
            print('按下了鼠标中键')
    def mouseReleaseEvent(self, arg: QMouseEvent):
        btn = arg.button()
        if btn == Qt.MouseButton.LeftButton:
            print('释放了鼠标左键')
        elif btn == Qt.MouseButton.RightButton:
            print('释放了鼠标右键')
        elif btn == Qt.MouseButton.MiddleButton:
            print('释放了鼠标中键')

假设在窗口上按下了鼠标右键,控制台会打印“按下了鼠标右键”;当松开右键后,控制台会打印“释放了鼠标右键”。

4.5

键盘事件

QWindow类中与键盘事件有关的方法有两个:当键盘上某个按键被按下时会调用keyPressEvent方法;当键盘上某个按键被释放时会调用 keyReleaseEvent方法。

事件参数为QKeyEvent类,可以通过 key 方法获得当前被按下(或释放)的按键编码。要注意的是该编码并非常见的键盘码,它与系统底层相互独立。key 方法返回整型数值,可以与 Qt.Key枚举进行比较,以确定是哪个按键触发了事件,例如:

def keyPressEvent(self, arg: QKeyEvent):
    if arg.key() == Qt.Key.Key_Return:
        print("你按下了【Enter】键")
def keyReleaseEvent(self, arg: QKeyEvent):
    if arg.key() == Qt.Key.Key_Return:
        print("你释放了【Enter】键")

Key_Return代表【Return】键(Enter键)。上述代码判断用户按下和释放的键是否为【Enter】键。

key方法返回的按键编码不区分大小写,例如下面这段代码,不管键盘输入的字符是“Q”还是“q”,if语句的条件都成立。

def keyPressEvent(self, arg: QKeyEvent):
    if arg.key() == Qt.Key.Key_Q:
        .....
def keyReleaseEvent(self, arg: QKeyEvent):
    if arg.key() == Qt.Key.Key_Q:
        ..

若需要直接获取键盘输入的字符(而不是编码),可以调用text方法。此方法返回按键所表示的字符(一般为可见字符,即可打印字符)。Ctrl、Shift、Alt等非可见字符可能返回空字符串。

4.5.1 示例:绘制键盘输入的字符

本示例将实现一个自定义窗口。该窗口接收键盘输入,然后把键盘输入的字符绘制到窗口上。

定义 DemoWindow类,派生自 QRasterWindow 类。

class DemoWindow(QRasterWindow):
    def __init__(self, parent:QWindow = None):
        super().__init__(parent)
        #默认文本
        self._text = ''
    def paintEvent(self, event: QPaintEvent):
        painter = QPainter(self)
        rect = event.rect()
        #背景颜色
        painter.setBackground(QColor('white'))
        #清除画布
        painter.eraseRect(rect)
        #设置画笔
        painter.setPen('black')
        #文本绘制区域
        textRect = rect.adjusted(10, 10, -10, -10)
        #绘制文本
        painter.drawText(textRect, Qt.TextFlag.TextWrapAnywhere, self._text)
        painter.end()
    def keyPressEvent(self, arg: QKeyEvent):
        #获取输入的字符
        input = arg.text()
        #如果是可见字符,追加到_text变量中
        if input.isprintable():
            self._text = self._text + input
        else:
            #如果是【Backspace】键,删去一个字符
            if arg.key() == Qt.Key.Key_Backspace:
                self._text = self._text[:-1]
            elif arg.key() == Qt.Key.Key_Return or arg.key() == Qt.Key.Key_Enter:
                #如果是【Enter】键,追加换行符
                self._text = self._text + '\n'
        #强制窗口重新绘制
        self.update()

重写 paintEvent 方法,将文本(_text 变量)内容绘制到窗口上。在调用 drawText 方法时指定了Qt.TextFlag.TextWrapAnywhere标志,表示文本在任何位置都可以进行换行(不局限于单词边界)。

DemoWindow类还重写了keyPressEvent方法,当发生键盘按下事件时会调用该方法。本示例将完成以下工作。

  1. 如果键盘输入的是可打印字符(可见字符),就把字符追加到_text变量中。
  1. 如果是不可见字符(控制字符),将进一步处理。
  1. 如果按键是【Enter】键(包括数字键盘中的【Enter】键),就在 _text 变量的后面追加换行符(\n)。
  1. 如果按键是【Backspace】键,就删除 _text 变量中的最后一个字符(通过切片 [:-1] 排除最后一个字符)。
  1. 调用 update 方法刷新窗口,使窗口发生重绘,paintEvent 方法被调用,重新绘制 _text 变量中的文本。
图 4-7
图4-7 绘制键盘输入的字符
图4-7 绘制键盘输入的字符

示例的运行结果如图4-7所示。

4.5.2 示例:通过方向键移动窗口

本示例的功能是通过上、下、左、右箭头按键来移动窗口。例如,按下【→】键,窗口就会向右移动。

重写 keyPressEvent 方法。

def keyPressEvent(self, event: QKeyEvent):
    #获取窗口的当前位置
    curPos = self.position()
    curX, curY = curPos.x(), curPos.y()
    #窗口移动的步长
    step = 30
    #向上
    if event.key() == Qt.Key.Key_Up:
        newPos = QPoint(curX, curY - step)
    #向左
    elif event.key() == Qt.Key.Key_Left:
        newPos = QPoint(curX - step, curY)
    #向右
    elif event.key() == Qt.Key.Key_Right:
        newPos = QPoint(curX + step, curY)
    #向下
    elif event.key() == Qt.Key.Key_Down:
        newPos = QPoint(curX, curY + step)
    #检查窗口坐标是否超出屏幕范围
    screenRect = self.screen().geometry()
    if newPos.x()< screenRect.left(): newPos.setX(screenRect.left())
    if newPos.x() > (screenRect.right() - self.width()): newPos.setX(
        screenRect.right() - self.width())
    if newPos.y() < screenRect.top(): newPos.setY(screenRect.top())
    if newPos.y() > (screenRect.bottom() - self.height()): newPos.setY(
        screenRect.bottom() - self.height())
    #设置窗口的新位置
    self.setPosition(newPos)

首先通过position方法获得当前窗口的位置坐标,然后根据按键修改坐标点。窗口的新坐标保存在newPos变量中。在更新窗口坐标前要验证一下移动后的新位置是否超出当前屏幕(或桌面)范围,如果是则修正新坐标的值,保证窗口不会被移动到屏幕之外。

  1. 如果移动后X坐标小于屏幕的左边沿,就让窗口的X坐标保持在屏幕的左边沿处。
  2. 如果移动后X坐标大于屏幕右边沿与窗口宽度的差,则让窗口停靠在屏幕右边沿处。
  3. 如果移动后Y坐标小于屏幕的上边沿,则让窗口停靠在屏幕顶部。
  4. 如果移动后Y坐标大于屏幕底部与窗口高度的差,则让窗口停靠在屏幕底部。

最后调用 setPosition方法更新当前窗口的位置。

4.6

嵌套窗口

QWindow对象(包括其子类)之间可以建立层级关系——某个窗口可以嵌套在另一个窗口中。若B窗口嵌套在A窗口内,那么A为父窗口,B为子窗口。QWindow类的构造函数带有一个可选的parent参数,用于设置当前窗口的父窗口,例如:

windB = TestWindow(windA)

上述代码实例化 TestWindow 窗口(由 windB 变量引用),并以 windA 为父窗口,即 windA 是父窗口,windB是子窗口。调用 setParent方法也可以建立父子窗口。

windB.setParent(windA)

若将parent参数设置为None,可以解除父子窗口关系,windB恢复为顶层窗口。

4.6.1 示例:父子窗口

本示例创建了两个窗口类。FirstWindow类为父窗口,SecondWindow类为子窗口。当SecondWindow窗口被单击后,会解除与FirstWindow窗口的层级关系,成为顶层窗口。

下面的代码实现 FirstWindow类。

class FirstWindow(QRasterWindow):
    def paintEvent(self, paintEv: QPaintEvent):
        rect = paintEv.rect()
        painter = QPainter(self)
        #填充整个窗口
        painter.fillRect(rect, QColor('green'))
        painter.end()

接着实现 SecondWindow 类。

class SecondWindow(QRasterWindow):
    def paintEvent(self, paintEvent: QPaintEvent):
        painter = QPainter(self)
        #填充窗口
        painter.fillRect(paintEvent.rect(), QColor('red'))
        painter.end()
    def mousePressEvent(self, arg: QMouseEvent):
        if arg.button() == Qt.MouseButton.LeftButton:
            if self.parent() != None:
                #解除窗口的层级关系
                self.setParent(None)

SecondWindow 窗口重写 mousePressEvent 方法,当单击该窗口时,调用 setParent(None)使当前窗口脱离嵌套关系,成为顶层窗口。

在实例化时,把 FirstWindow窗口的实例通过构造函数传递给 SecondWindow 窗口,使 FirstWindow成为父窗口,SecondWindow为子窗口。

win1 = FirstWindow()
win1.setTitle("父窗口")
win1.resize(420, 360)
win2 = SecondWindow(win1)
win2.setTitle("子窗口")
win2.setGeometry(130, 100, 240, 185)
#显示窗口
win1.show()
win2.show()

两个窗口对象嵌套后的效果如图4-8所示。子窗口在呈现时会隐藏标题栏和边框,只显示窗口内容区域。

图 4-8
图 4-8 嵌套窗口
图 4-8 嵌套窗口

4.6.2 示例:按钮

窗口上可与用户进行交互的元素一般称为控件(control)。控件实际上是嵌套在主窗口内的子窗口。

本示例将实现MyButton类,它是一个按钮控件,主要行为是当鼠标按下并释放时发出clicked信号。

  1. 定义MainWindow类,将作为应用程序的主窗口,也是MyButton控件的父窗口。
class MainWindow(QRasterWindow):
    def paintEvent(self, event: QPaintEvent):
        rect = event.rect()
        with QPainter(self) as painter:
            #填充背景
            painter.fillRect(rect, QColor('gold'))
  1. 定义MyButton类,它表示按钮控件。
class MyButton(QRasterWindow):
  1. 在MyButton类的__init__ 方法中初始化一些变量值。
def __init__(self, parent: QWindow = None):
    super().__init__(parent)
    #设置窗口标志,表示子窗口
    self.setFlag(Qt.WindowType.SubWindow, True)
    #初始值
    #常规状态下的背景颜色
    self._bcolor = QColor('blue')
    #常规状态下的前景颜色
    self._fcolor = QColor('lightgray')
    #按钮上显示的文本
    self._text = 'Button'
    #按钮上文本的对齐方式,默认居中
    self._alignment = Qt.AlignmentFlag.AlignCenter
    #表示鼠标指针是否悬浮在按钮上
    self._hover = False
    #鼠标指针悬浮在按钮上时呈现的背景颜色
    self._hbcolor = QColor('skyblue')
    #鼠标指针悬浮在按钮上时呈现的前景颜色
    self._hfcolor = QColor('yellow')
    self.setVisible(True) #可见状态
    self.resize(100, 36) #调整大小
  1. 定义一组属性,如常规状态下的背景颜色、前景颜色,鼠标指针悬浮状态下的背景颜色、前景颜色等。
#属性:背景颜色
@property
def backColor(self) -> QColor:
    return self._bcolor
@backColor.setter
def backColor(self, color: QColor):
    self._bcolor = color
#属性:前景颜色
@property
def foreColor(self) -> QColor:
    return self._fcolor
@foreColor.setter
def foreColor(self, color: QColor):
    self._fcolor = color
#属性:按钮上的文本
@property
def text(self) -> str:
    return self._text
@text.setter
def text(self, s: str):
    self._text = s
#属性:文本的对齐方式
@property
def textAlignment(self) -> Qt.AlignmentFlag:
    return self._alignment
@textAlignment.setter
def textAlignment(self, align: Qt.AlignmentFlag):
    self._alignment = align
#属性:当鼠标指针悬浮时的背景颜色
@property
def hoverBackColor(self) -> QColor:
    return self._hbcolor
@hoverBackColor.setter
def hoverBackColor(self, color: QColor):
    self._hbcolor = color
#属性:当鼠标指针悬浮时的前景颜色
@property
def hoverForeColor(self) -> QColor:
    return self._hfcolor
@hoverForeColor.setter
def hoverForeColor(self, color: QColor):
    self._hfcolor = color
  1. 定义 clicked 信号。
clicked = Signal()
  1. 重写event方法,处理鼠标指针移入和移出事件。
def event(self, event: QEvent) -> bool:
    #鼠标指针进入控件
    if event.type() == QEvent.Type.Enter:
        self._hover = True
        self.update()
        return True
    #鼠标指针离开控件
    if event.type() == QEvent.Type.Leave:
        self._hover = False
        self.update()
        return True
    return super().event(event)

当鼠标指针移入按钮控件时,发生Enter事件,当指针移出时发生Leave事件。update方法强制重新绘制窗口内容。

  1. 重写paintEvent方法,绘制按钮的内容(背景色/前景色/文本)。
def paintEvent(self, event: QPaintEvent):
    painter = QPainter(self)
    rect = event.rect()
    #填充背景
    if self._hover == True:
        painter.fillRect(rect, self._hbcolor)
    else:
        painter.fillRect(rect, self._bcolor)
    #设置画笔(使用前景色)
    if self._hover == True:
        painter.setPen(self._hfcolor)
    else:
        painter.setPen(self._fcolor)
    #绘制边框
    painter.drawRect(rect)
    #绘制文本
    painter.drawText(rect, self._text, self._alignment)
    painter.end()
  1. 重写mousePressEvent、mouseReleaseEvent方法,处理鼠标单击事件。当鼠标左键被按下后,先把触发事件的设备ID(一般是当前鼠标)暂时存到__cacheDevid变量中,然后把_mousePressed变量修改为True,表示已按键已按下。当鼠标左键释放时,检查之前保存在__cacheDevid变量中的设备ID是否与正在释放按键的设备一致。如果一致,说明左键按下与释放是同一设备操作的,可以认为是完整的单击事件,于是发出clicked信号。
def mouseReleaseEvent(self, ev: QMouseEvent):
    if ev.button() == Qt.MouseButton.LeftButton:
        #按键按下与释放是否为同一设备
        if self.__cacheDevid == ev.device().systemId() and self._mousePressed == True:
            #发出信号
            self.clicked.emit()
    self._mousePressed = False
  1. 实例化 MainWindow 类。
win = MainWindow()
win.setTitle("按钮示例")
win.resize(411, 305)
win.setPosition(810, 525)
  1. 实例化 MyButton 类。
btn = MyButton()
#设置位置与大小
btn.setGeometry(35, 130, 96, 40)
#设置文本
btn.text = "请单击"
#设置父窗口
btn.setParent(win)
#绑定信号与槽
btn.clicked.connect(lambda: print('你已单击按钮'))

MyButton 对象的 clicked 信号绑定的槽对象为 lambda 表达式,用 print 函数打印文本“你已单击按钮”。

  1. 显示窗口。
win.show()

示例的运行效果如图4-9所示。用鼠标或类似鼠标的设备单击按钮,就会看到控制台窗口中有文本输出。

图 4-9
图 4-9 按钮控件
图 4-9 按钮控件