PyQt5 - QWidgets部件进阶教程之塑形时钟

废话

部件模板通过限制绘制的可用区域,来自定义顶层部件的形状。在一些视窗系统中,设置一些窗口标志可以使窗口装饰关闭(如标题栏、窗口矿建、按钮等),这样就能实现创建指定形状的视窗。该案例中,我们使用这一特性来创建一个模拟时钟的圆形视窗。
当然该案例的视窗不提供文件菜单或关闭按钮,我们提供一个包含退出口的环境菜单,这样就能使案例关闭,在窗口上点击鼠标右键就可以打开这个菜单。


定义ShapedClock类

ShapedClock类基于AnalogClock类案例,本例中不再重新展示相同代码说明,如有需要可参考链接。

class ShapedClock(QWidget):
    def __init__(self):
        super(ShapedClock, self).__init__()

        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(1000)

        self.quitAction = QAction('E&xit', self)
        self.quitAction.setShortcut('Ctrl+Q')
        self.quitAction.triggered.connect(qApp.quit)
        self.addAction(self.quitAction)

        self.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.setToolTip('Drag the clock with the left mouse button.\n '
                        'Use the right mouse button to open a context menu.')
        self.setWindowTitle('Shaped Analog Clock')
  • paintEvent()的重写与AnalogClock类相同,实现sizeHint(),这样就不用明确的调整部件大小,同样为调整尺寸事件提供一个事件处理程序,如果时钟调整大小,这允许我们更新模板。
  • 视窗不包含标题栏,我们重写mouseMoveEvent()和mousePressEvent(),这可以实现时钟在屏幕内拖动,dragPosition变量让我们保持追踪用户最后在部件上点击的位置。
  • 设置一个timer,并连接到部件的update()槽
  • 我们告知视窗管理器,部件不再用视窗框架装饰,这通过在部件上设置FramelessWindowHint标志实现。然后我们需要提供一个用户在屏幕内移动时钟的方法。鼠标按钮事件交由mousePressEvent()操作。
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.dragPosition = event.globalPos() - self.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() and Qt.LeftButton:
            self.move(event.globalPos() - self.dragPosition)
            event.accept()
  • 如果鼠标左键点击在部件上,我们记录‘部件框架的顶-左位置’和‘点鼠标点击时的点’之间的全局坐标位移。如果用户点住鼠标左键时移动,则会使用位移。当我们作用在事件上后,会通过accept()函数接受它的值。
  • 如果鼠标在部件上移动,会调用mouseMoveEvent()处理程序。如果点住左键移动,部件的顶-左角则移动到一点,该点是从‘全局坐标中的当前光标位置’减去‘拖动位置’获得。如果我们拖动部件,同样接受事件。
    def resizeEvent(self, event):
        side = max(self.width(), self.height())
        maskedRegion = QRegion(self.width() / 2 - side / 2, self.height() / 2 - side / 2, side, side, QRegion.Ellipse)
        self.setMask(maskedRegion)

    def sizeHint(self):
        return QSize(100, 100)
  • 当时在部件中心后绘制圆形钟面后,我们将这个作为模板原型。
  • 缺少视窗框架可能导致用户在一些平台上难以调整部件大小,但这不一定是不可能的。当部件尺寸调整,resizeEvent()函数总是可以确保部件模板更新,另外可以保证当部件首次显示时可以正确设置。
  • 最终我们为部件实现sizeHint(),这样当首次显示时塔克已给出一个合理的默认尺寸。

最终代码

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class ShapedClock(QWidget):
    def __init__(self):
        super(ShapedClock, self).__init__()

        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(1000)

        self.quitAction = QAction('E&xit', self)
        self.quitAction.setShortcut('Ctrl+Q')
        self.quitAction.triggered.connect(qApp.quit)
        self.addAction(self.quitAction)

        self.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.setToolTip('Drag the clock with the left mouse button.\n '
                        'Use the right mouse button to open a context menu.')
        self.setWindowTitle('Shaped Analog Clock')

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.dragPosition = event.globalPos() - self.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() and Qt.LeftButton:
            self.move(event.globalPos() - self.dragPosition)
            event.accept()

    def paintEvent(self, QPaintEvent):
        hourHand = [
            QPoint(7, 8),
            QPoint(-7, 8),
            QPoint(0, -40)
        ]
        minuteHand = [
            QPoint(7, 8),
            QPoint(-7, 8),
            QPoint(0, -70)
        ]

        hourColor = QColor(127, 0, 127)
        minuteColor = QColor(0, 127, 127, 191)

        side = min(self.width(), self.height())
        time = QTime.currentTime()

        painter = QPainter()
        painter.begin(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.translate(self.width() / 2, self.height() / 2)
        painter.scale(side / 200.0, side / 200.0)

        painter.setPen(Qt.NoPen)
        painter.setBrush(hourColor)
        painter.save()
        painter.rotate(30.0 * (time.hour() + time.minute() / 60))
        painter.drawConvexPolygon(QPolygon(hourHand))
        painter.restore()

        painter.setPen(hourColor)
        for i in range(12):
            painter.drawLine(88, 0, 96, 0)
            painter.rotate(30.0)

        painter.setPen(Qt.NoPen)
        painter.setBrush(minuteColor)
        painter.save()
        painter.rotate(6.0 * (time.minute() + time.second() / 60))
        painter.drawConvexPolygon(QPolygon(minuteHand))
        painter.restore()

        painter.setPen(minuteColor)
        for i in range(60):
            if i % 5 != 0:
                painter.drawLine(92, 0, 96, 0)
            painter.rotate(6.0)

        painter.end()

    def resizeEvent(self, event):
        side = max(self.width(), self.height())
        maskedRegion = QRegion(self.width() / 2 - side / 2, self.height() / 2 - side / 2, side, side, QRegion.Ellipse)
        self.setMask(maskedRegion)

    def sizeHint(self):
        return QSize(100, 100)

app = QApplication(sys.argv)
clock = ShapedClock()
clock.show()
app.exec_()

最终效果

这里写图片描述


视窗模板注意事项

当QRegion允许创建任意符合区域后,部件模板可以创建成适配大部分异型视窗,甚至允许部件中间显示一个洞!部件模板同样可以使用位图的内容进行构造,来定义部件的不透明的部分。位图有一个alpha通道,可以通过QPixmap.mask()获取。


问题说明

画出来的时钟面我突然发现不圆,不知道是算法的问题,还是视觉差的问题,这里我作为一个Bug记录下来,后续会跟进处理。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值