【PyQt】(自制类)简易的控件画布

编程入门 行业动态 更新时间:2024-10-26 20:22:39

【PyQt】(自制类)简易的控件<a href=https://www.elefans.com/category/jswz/34/1766074.html style=画布"/>

【PyQt】(自制类)简易的控件画布

说一下标题的意思,就是一个可往上面放QtWidgets控件(例如QLabelQPushButton)并且画布可拖拽缩放的一个简易画布类。
强调一下的就是,这和涂鸦画布(类比于win自带的画图软件)不是同个东西。

只不过通过这个自制类我明白了一点的就是控件数量太多会造成明显卡顿(哪怕控件数量也才几百个),这让我对自己写的鸡肋玩意儿的整活程度又上升了一个档次(想想都鸡肋,写的这破玩意儿用哪才合适。



Python代码:

#XJ_Canvas.py
import numpy as np
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt,QRect
from XJ_MouseStatus import *__all__=['XJ_Canvas']
class XJ_Canvas(QWidget):__objs={}#{weight:[obj,...]}__weights={}#{obj:weight}__poses={}#{obj:QRect}#不知道要咋起名,随便来算了__matrix=None#转换矩阵(np.array),逻辑坐标→显示坐标__mouseStatus=None#XJ_MouseStatusdef __init__(self):super().__init__()self.__objs={}self.__weights={}self.__poses={}self.__matrix=np.array([[3,0,0],[0,3,0],[0,0,1]])self.__mouseStatus=XJ_MouseStatus()def Opt_ObjectAdd(self,obj,weight=1,pos=QRect(0,0,1,1)):#添加控件(需要附加控件的显示权重,数值越小越优先),pos为控件位置if(isinstance(obj.parent(),XJ_Canvas)):#obj曾出现在别的画布中obj.parent().Opt_ObjectRemove(obj)obj.setParent(self)objs=self.__objsweigs=self.__weightsposes=self.__poseslst=objs.setdefault(weight,[])if(obj in lst):lst.remove(obj)lst.insert(0,obj)weigs[obj]=weightposes[obj]=poskeys=sorted(objs)index=keys.index(weight)if(index==0):#顶级obj.raise_()else:#置后key_front=keys[index-1]obj_front=objs[key_front][-1]obj.stackUnder(obj_front)obj.show()self.__Update(obj)def Opt_ObjectRemove(self,obj):#移除控件if(obj in self.__weights):self.__objs[self.__weights[obj]].pop(obj)self.__weights.pop(obj)obj.setParent(None)obj.hide()def Opt_OrderAlter(self,obj_move,obj_target,*,Above=False,Below=False):#使控件obj_move位置置于obj_target之上/下(取决于Above/Below取值),同时修改obj_move的权objs=self.__objsweigs=self.__weightsif(Above^Below):#有一值为真if(obj_move in weigs and obj_target in weigs):#确保俩控件都在画布中obj_m=obj_moveobj_t=obj_targetweig_m=weigs[obj_move]weig_t=weigs[obj_target]lst=objs[weig_t]index=lst.index(weig_t)objs[weig_m].remove(obj_m)weigs[obj_m]=weig_tobj_m.stackUnder(obj_t)if(Above):#置上obj_t.stackUnder(obj_m)lst.insert(index,obj_m)else:#置下lst.insert(index+1,obj_m)def Get_ObjectExist(self,obj):#判断控件是否存在return obj in self.__weightsdef Get_ObjectPosition(self,obj):#获取控件位置(控件不存在将返回无效QRect)if(obj not in self.__weights):return QRect()return self.__poses[obj]def Get_ObjectWeight(self,obj):#获取控件权重return self.__weights[obj]def Set_ObjectWeight(self,obj,weight):#设置控件权重(本质调用Opt_ObjectAdd)self.Opt_ObjectAdd(obj,weight)return Truedef Set_ObjectPosition(self,obj,pos):#设置控件位置(pos为QRect)if(obj not in self.__weights):return Falseif(not isinstance(pos,QRect)):#不是QRect,抛出错误(趁早修改错误调用)raise TypeError("非QRect对象",pos)self.__poses[obj]=posself.__Update(obj)return Truedef __Update(self,*objs):#更新指定控件。如果objs为空那么将更新所有对象if(not objs):objs=self.__weights.keys()for obj in objs:pos=self.__poses[obj]mat=np.array([[pos.left(),pos.top(),1],[pos.right(),pos.bottom(),1]])mat=mat.dot(self.__matrix)/self.__matrix[2][2]L,T,_=mat[0]R,B,_=mat[1]obj.setGeometry(L,T,R-L,B-T)obj.update()def wheelEvent(self,event):pos=event.pos()rate=1+event.angleDelta().y()/1000if(self.__matrix[0][0]<0.05 and rate<1):#防止过度缩小returnself.__matrix=self.__matrix.dot(np.array([[rate,0,0],[0,rate,0],[pos.x()*(1-rate),pos.y()*(1-rate),1]]))#以鼠标位置为中心进行缩放self.__Update()def mousePressEvent(self,event):ms=self.__mouseStatusms.Opt_Update(event)#更新鼠标状态def mouseReleaseEvent(self,event):#对象的点击事件在鼠标抬起时触发,而不在鼠标按下时触发,这样做是为了避免和拖拽操作相冲突ms=self.__mouseStatusms.Opt_Update(event)#更新鼠标状态if(not ms.Get_HasMoved()):#鼠标未发生拖拽行为event.ignore()#让Object对象处理点击释放操作def mouseMoveEvent(self,event):ms=self.__mouseStatusms.Opt_Update(event)#更新鼠标状态if(ms.Get_PressButtonStatus()[0]==Qt.LeftButton):#左键拖拽event.ignore()offset=ms.Get_MoveDelta(False)self.__matrix[2]=self.__matrix[2]+[offset.x(),offset.y(),0]self.__Update()if __name__=='__main__':import sysfrom PyQt5.QtWidgets import QApplication,QWidget,QLabel,QLineEdit,QPushButtonfrom XJ_Object import *app = QApplication(sys.argv)class Test(XJ_Object,QLabel):#需要继承XJ_Object。虽然不继承也没啥,一样能往XJ_Canvas塞Qt原生控件,就是点到控件时没法拖拽画布而已# class Test(Object,QPushButton):passcv= XJ_Canvas()cv.show()for x in range(30):for y in range(30):t=Test(f"{x},{y}")cv.Opt_ObjectAdd(t,pos=QRect(10*x,10*y,10,10))sys.exit(app.exec())
#XJ_MouseStatus.py
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import QPoint,Qt,QObject
from PyQt5.QtGui import QMouseEvent__all__=['XJ_MouseStatus']class XJ_MouseStatus(QObject):#mousePressEvent、mouseMoveEvent和mouseReleaseEvent特供。只处理单键(多键行为请在外部代码控制)longClick=pyqtSignal()#鼠标原地不动长按时触发__antiJitter=5#防抖,当鼠标点击位置与鼠标当前位置的曼哈顿距离不超过该值时仍将鼠标视为不动状态__doubleClickInterval=500#双击间隔(ms)__longPressInterval=500#长按间隔(ms)__record={'lastPress':None,#上一次按下时的信息'lastMouse':None,#上一次的鼠标信息'currMouse':None,#当前鼠标信息}__press=[QMouseEvent.MouseButtonRelease,QMouseEvent.MouseButtonPress,QMouseEvent.MouseButtonDblClick]#偷懒用的__move=False#用于判断是否长按__timerID=0#鼠标按下时对应的定时器class __Data:pos=None#鼠标位置btn=None#鼠标按键(左中右)pressStatus=None#鼠标当前按下状态(单双击/抬起)timeStamp=None#鼠标事件时间刻def __init__(self,event):self.pos=event.globalPos()self.btn=event.button()self.pressStatus=event.MouseButtonReleaseself.timeStamp=event.timestamp()def __init__(self,*arg):super().__init__(*arg)record=self.__record.copy()fakeEvent=QMouseEvent(QMouseEvent.MouseButtonRelease,QPoint(0,0),Qt.NoButton,Qt.NoButton,Qt.NoModifier)data=self.__Data(fakeEvent)data.timeStamp-=self.__doubleClickInterval#小防,避免开局单击时触发双击行为record['lastMouse']=datarecord['currMouse']=datarecord['lastPress']=dataself.__record=recorddef timerEvent(self,event):record=self.__recordpress=self.__presstId=event.timerId()cId=self.__timerIDself.killTimer(event.timerId())if(cId==tId):#当前定时器if(not self.__move and record['currMouse'].pressStatus!=press[0]):#未发生移动,未抬起鼠标,触发长按信号self.longClick.emit()def Set_DoubleClickInterval(self,interval):#设置双击时间间隔(ms)self.__doubleClickInterval=intervaldef Set_LongPressInterval(self,interval):#设置长按时间间隔(ms)self.__longPressInterval=intervaldef Set_AntiJitter(self,val):#设置防抖值self.__antiJitter=val if val>0 else 0def Get_Position(self):#返回鼠标坐标。是屏幕坐标(global),需要使用QWidget.mapFromGlobal(QPoint)自行转换为控件相对坐标return self.__record['currMouse'].posdef Get_PressButtonStatus(self):#返回当前鼠标的键(左中右)以及按下状态(单击/双击/抬起)return self.__record['currMouse'].btn,self.__record['currMouse'].pressStatusdef Get_MoveDelta(self,total=True,strict=True):#返回鼠标移动量(仅鼠标按下时有效),为QPoint对象press=self.__pressrecord=self.__recorddata_curr=record['currMouse']if(data_curr.pressStatus!=press[0]):#说明鼠标按下if(not strict or self.__move):#严格模式下,仅判定发生移动时计算移动量p1=record['currMouse'].posif(total):p2=record['lastPress'].poselse:p2=record['lastMouse'].posreturn QPoint(p1.x()-p2.x(),p1.y()-p2.y())return QPoint(0,0)def Get_HasMoved(self):#判断是否发生移动(毕竟用Get_MoveDelta来判断移动的发生是有点麻烦,还不如多一个函数return self.__movedef Opt_Update(self,event):#更新状态press=self.__pressrecord=self.__recorddata_curr=self.__Data(event)if(event.type()==press[1] or event.type()==press[2]):#单/双击self.__move=Falsedata_old=record['lastPress']data_curr.pressStatus=press[1]if(data_old.btn==data_curr.btn):#同键位按下if(data_curr.timeStamp-data_old.timeStamp<self.__doubleClickInterval):#在时间间隔内if(data_old.pressStatus!=press[2]):#没有双击过data_curr.pressStatus=press[2]#双击record['lastPress']=data_currrecord['lastMouse']=data_currrecord['currMouse']=data_currself.__timerID=self.startTimer(self.__longPressInterval)else:#移动/抬起data_curr.btn=event.buttons()data_curr.pressStatus=record['lastMouse'].pressStatusif(event.type()==press[0]):#抬起if(data_curr.btn==Qt.NoButton):#确保无按键按下时设置为Releasedata_curr.pressStatus=press[0]data_curr.btn=event.button()else:#移动(QMouseEvent.MouseMove)if(data_curr.pressStatus!=press[0] and not self.__move):#判断有无发生拖拽delta=self.Get_MoveDelta(strict=False)if(abs(delta.x())+abs(delta.y())>self.__antiJitter):self.__move=Truerecord['currMouse'].pos=record['lastPress'].posrecord['lastMouse']=record['currMouse']record['currMouse']=data_currif __name__=='__main__':import sysfrom PyQt5.QtWidgets import QApplication,QWidgetclass Test(QWidget):__mouseStatus=Nonedef __init__(self,*arg):super().__init__(*arg)ms=XJ_MouseStatus()ms.longClick.connect(lambda:print("<LongClick!>"))self.__mouseStatus=msdef __EasyPrint(self):press={QMouseEvent.MouseButtonRelease:"Release",QMouseEvent.MouseButtonPress:"Press",QMouseEvent.MouseButtonDblClick:"DblClick",}button={Qt.LeftButton:'Left',Qt.MidButton:'Middle',Qt.RightButton:'Right',}tPoint=lambda point:(point.x(),point.y())tBtn=lambda btn:[button[key] for key in button if key&btn]tBtnStatus=lambda status:(tBtn(status[0]),press[status[1]])ms=self.__mouseStatuspos=tPoint(self.mapFromGlobal(ms.Get_Position()))moveDelta=tPoint(ms.Get_MoveDelta())btnStatus=tBtnStatus(ms.Get_PressButtonStatus())print(f'pos{pos},\tdelta{moveDelta},\t{btnStatus[0]}-{btnStatus[1]}')if(btnStatus[1]=='Release'):print()def mousePressEvent(self,event):self.__mouseStatus.Opt_Update(event)self.__EasyPrint()def mouseMoveEvent(self,event):self.__mouseStatus.Opt_Update(event)self.__EasyPrint()def mouseReleaseEvent(self,event):self.__mouseStatus.Opt_Update(event)self.__EasyPrint()app = QApplication(sys.argv)t=Test()t.show()sys.exit(app.exec())
#XJ_Object.py
__all__=['XJ_Object']
class XJ_Object:#这个类的主要作用是鼠标事件逆传递,即先让父控件处理然后本控件才进行动作def mousePressEvent(self,event,defaultInvoke=True):canvas=self.parent()if(canvas):canvas.mousePressEvent(event)#先让上级调用if(not event.isAccepted() and defaultInvoke):super().mousePressEvent(event)event.accept()def mouseReleaseEvent(self,event,defaultInvoke=True):canvas=self.parent()if(canvas):canvas.mouseReleaseEvent(event)#先让上级调用if(not event.isAccepted() and defaultInvoke):super().mouseReleaseEvent(event)event.accept()def mouseMoveEvent(self,event,defaultInvoke=True):canvas=self.parent()if(canvas):canvas.mouseMoveEvent(event)#先让上级调用if(not event.isAccepted() and defaultInvoke):super().mouseMoveEvent(event)event.accept()

这里简单说明一下,上面代码有三个类:
  • XJ_Canvas:画布类,支持控件的层级放置+画布拖拽+滚轮缩放,控件需要额外继承XJ_Object以避免鼠标点中控件时无法拖拽画布的问题。
  • XJ_MouseStatus:用于支持XJ_Canvas,大幅简化鼠标点击/拖拽的逻辑代码,这个类与我之前写的博文关联:【PyQt】(自制类)处理鼠标点击逻辑
  • XJ_Object,实现鼠标点击事件的逆传递(没找到更好的实现方法就此作罢)。关于Qt控件的事件传递可以参考这篇博客:[博客园]Qt事件系统之一:Qt中的事件处理与传递



测试代码和运行结果:

#Main.py
import sys
from PyQt5.QtWidgets import QApplication,QLabel,QPushButton
from XJ_Object import *
from XJ_Canvas import *if __name__=='__main__':app = QApplication(sys.argv)# class Test(XJ_Object,QLabel):class Test(XJ_Object,QPushButton):passcv= XJ_Canvas()cv.show()for x in range(30):#塞进30*30=900个控件for y in range(30):t=Test(f"{x},{y}")cv.Opt_ObjectAdd(t,pos=QRect(10*x,10*y,10,10))sys.exit(app.exec())


上面测试代码中往画布放置了30*30=900个控件,在画布拖拽和缩放时已经出现了极为明显的卡顿(画面响应速度超过1秒)。无助,且鸡肋。

感觉有点用 但依旧是一坨垃圾,就放到博客里头 来污染网络环境




未经本人同意不得私自转载,本文发布于CSDN:

更多推荐

【PyQt】(自制类)简易的控件画布

本文发布于:2023-11-15 06:29:03,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1595269.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:画布   控件   简易   PyQt

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!