You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5可调整大小无框窗口拖拽卡顿问题求助

问题:Qt无框Widget拖拽调整大小卡顿/闪烁

我编写了一段代码,实现Qt无框Widget支持拖拽边缘调整大小,但调整窗口大小和移动窗口时,界面出现卡顿/闪烁现象(查看下方动图,注意右侧和底部边缘):

Widget拖拽时卡顿闪烁

以下是我的代码:

import sys
from typing import Dict, Tuple

from PyQt5.QtCore import QPoint, QRect, Qt, QMargins
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QApplication, QWidget

dragMargin = 8
top = 0
bot = 1
left = 2
right = 3
c = Qt.CursorShape

class ScaleableWindowFrame(QWidget):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.setMouseTracking(True)
        self.prevDir = (None, None)
        self.lastPos : QPoint= None

    directionCursor: Dict[Tuple[int, int], Qt.CursorShape] = {
        (top, left): c.SizeFDiagCursor,
        (top, None): c.SizeVerCursor,
        (top, right): c.SizeBDiagCursor,
        (None, left): c.SizeHorCursor,
        (None, right): c.SizeHorCursor,
        (bot, left): c.SizeBDiagCursor,
        (bot, None): c.SizeVerCursor,
        (bot, right): c.SizeFDiagCursor,
        (None, None): c.ArrowCursor,
    }

    def mousePressEvent(self, a0: QMouseEvent) -> None:
        
        if a0.buttons() == Qt.MouseButton.LeftButton:
            self.lastPos = a0.globalPos()
        a0.accept()

    def mouseMoveEvent(self, a0: QMouseEvent) -> None:
        if a0.buttons() == Qt.MouseButton.NoButton:
            self.handleHover(a0)
        elif a0.buttons() == Qt.MouseButton.LeftButton:
            self.handleDrag(a0)
        a0.accept()

    def handleDrag(self, a0:QMouseEvent):
        diff = a0.globalPos() - self.lastPos
        
        dx = diff.x()
        dy = diff.y()
        
        methods = {
            (top, left): lambda: (self.setGeometry(QRect(self.pos() + QPoint(dx, dy), self.size().grownBy(QMargins(0,0, -dx, -dy))))),
            (top, None): lambda: (self.setGeometry(QRect(self.pos() + QPoint(0, dy), self.size().grownBy(QMargins(0,0, 0, -dy))))) ,
            (top, right):lambda: (self.setGeometry(QRect(self.pos() + QPoint(0, dy), self.size().grownBy(QMargins(0,0, dx, -dy))))) ,
            (None, left):lambda: (self.setGeometry(QRect(self.pos() + QPoint(dx, 0), self.size().grownBy(QMargins(0, 0, -dx, 0))))),
            (None, right):lambda: (self.setGeometry(QRect(self.pos(), self.size().grownBy(QMargins(0,0, dx, 0))))),
            (bot, left): lambda: (self.setGeometry(QRect(self.pos() + QPoint(dx, 0), self.size().grownBy(QMargins(0,0, -dx, dy))))),
            (bot, None):lambda: (self.setGeometry(QRect(self.pos(), self.size().grownBy(QMargins(0,0, 0, dy))))),
            (bot, right): lambda: (self.setGeometry(QRect(self.pos(), self.size().grownBy(QMargins(0,0, dx, dy))))),
            (None, None): lambda: (),
        }
        
        methods[self.prevDir]()
        self.lastPos = a0.globalPos()
        a0.accept()
    
    def handleHover(self, a0: QMouseEvent):
        # first determine which edge the mouse cursor is on
        p = a0.pos()
        # vertical direction
        if p.y() < dragMargin:
            v = top
        elif self.height() - p.y() < dragMargin:
            v = bot
        else:
            v = None

        # horizontal direction
        if p.x() < dragMargin:
            h = left
        elif self.width() - p.x() < dragMargin:
            h = right
        else:
            h = None

        direction = (v, h)
        if direction != self.prevDir:
            self.setCursor(ScaleableWindowFrame.directionCursor[(v, h)])
            self.prevDir = direction
        a0.accept()

if __name__ == '__main__':
    a = QApplication(sys.argv)
    w = ScaleableWindowFrame()
    w.setWindowFlag(Qt.FramelessWindowHint, True)
    w.resize(400, 400)
    w.show()
    sys.exit(a.exec())

请问我是否存在处理错误?或者有更优的实现方式?


问题分析与优化方案

卡顿原因

  1. 频繁触发重绘:每次鼠标移动都直接调用setGeometry修改窗口参数,导致界面频繁重绘,引发闪烁。
  2. 无最小尺寸限制:拖拽时可能让窗口尺寸过小,触发异常重绘逻辑。
  3. 冗余计算逻辑:用lambda表达式封装几何计算,每次拖拽都重复创建匿名函数,效率偏低。

优化后的代码

import sys
from typing import Dict, Tuple

from PyQt5.QtCore import QPoint, QRect, Qt, QMargins
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QApplication, QWidget

dragMargin = 8
top = 0
bot = 1
left = 2
right = 3
c = Qt.CursorShape

class ScaleableWindowFrame(QWidget):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.setMouseTracking(True)
        self.prevDir = (None, None)
        self.lastPos: QPoint = None
        # 设置窗口最小尺寸,避免拖拽过度缩小
        self.setMinimumSize(100, 100)
        # 启用双缓冲,减少界面闪烁
        self.setAttribute(Qt.WA_StyledBackground, True)

    directionCursor: Dict[Tuple[int, int], Qt.CursorShape] = {
        (top, left): c.SizeFDiagCursor,
        (top, None): c.SizeVerCursor,
        (top, right): c.SizeBDiagCursor,
        (None, left): c.SizeHorCursor,
        (None, right): c.SizeHorCursor,
        (bot, left): c.SizeBDiagCursor,
        (bot, None): c.SizeVerCursor,
        (bot, right): c.SizeFDiagCursor,
        (None, None): c.ArrowCursor,
    }

    def mousePressEvent(self, a0: QMouseEvent) -> None:
        if a0.buttons() == Qt.MouseButton.LeftButton:
            self.lastPos = a0.globalPos()
        a0.accept()

    def mouseMoveEvent(self, a0: QMouseEvent) -> None:
        if a0.buttons() == Qt.MouseButton.NoButton:
            self.handleHover(a0)
        elif a0.buttons() == Qt.MouseButton.LeftButton:
            self.handleDrag(a0)
        a0.accept()

    def handleDrag(self, a0: QMouseEvent):
        diff = a0.globalPos() - self.lastPos
        dx = diff.x()
        dy = diff.y()
        current_rect = self.geometry()
        new_rect = current_rect.copy()

        v_dir, h_dir = self.prevDir

        # 处理垂直方向拖拽
        if v_dir == top:
            new_rect.setTop(current_rect.top() + dy)
            if new_rect.height() < self.minimumHeight():
                new_rect.setTop(current_rect.bottom() - self.minimumHeight())
        elif v_dir == bot:
            new_rect.setBottom(current_rect.bottom() + dy)
            if new_rect.height() < self.minimumHeight():
                new_rect.setBottom(current_rect.top() + self.minimumHeight())

        # 处理水平方向拖拽
        if h_dir == left:
            new_rect.setLeft(current_rect.left() + dx)
            if new_rect.width() < self.minimumWidth():
                new_rect.setLeft(current_rect.right() - self.minimumWidth())
        elif h_dir == right:
            new_rect.setRight(current_rect.right() + dx)
            if new_rect.width() < self.minimumWidth():
                new_rect.setRight(current_rect.left() + self.minimumWidth())

        # 仅当矩形变化时更新,减少重绘次数
        if new_rect != current_rect:
            self.setGeometry(new_rect)
        self.lastPos = a0.globalPos()
        a0.accept()

    def handleHover(self, a0: QMouseEvent):
        p = a0.pos()
        v = None
        if p.y() < dragMargin:
            v = top
        elif self.height() - p.y() < dragMargin:
            v = bot

        h = None
        if p.x() < dragMargin:
            h = left
        elif self.width() - p.x() < dragMargin:
            h = right

        direction = (v, h)
        if direction != self.prevDir:
            self.setCursor(self.directionCursor[direction])
            self.prevDir = direction
        a0.accept()

if __name__ == '__main__':
    a = QApplication(sys.argv)
    w = ScaleableWindowFrame()
    w.setWindowFlag(Qt.FramelessWindowHint, True)
    w.resize(400, 400)
    w.show()
    sys.exit(a.exec())

优化点说明

  • 减少重绘触发:先计算完整的新窗口矩形,仅当矩形确实变化时才调用setGeometry,避免不必要的界面更新。
  • 添加尺寸限制:设置窗口最小尺寸,防止拖拽时窗口过度缩小导致异常。
  • 启用双缓冲:通过WA_StyledBackground属性启用Qt内置双缓冲机制,直接缓解闪烁问题。
  • 简化计算逻辑:直接操作QRect的边界属性,替代原有的lambda表达式,代码更清晰且执行效率更高。

内容的提问来源于stack exchange,提问作者goldenotaste

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 00:48:25