说一下标题的意思,就是一个可往上面放QtWidgets
控件(例如QLabel
、QPushButton
)并且画布可拖拽缩放的一个简易画布类。
强调一下的就是,这和涂鸦画布(类比于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_MouseStatus
def __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.__objs
weigs=self.__weights
poses=self.__poses
lst=objs.setdefault(weight,[])
if(obj in lst):
lst.remove(obj)
lst.insert(0,obj)
weigs[obj]=weight
poses[obj]=pos
keys=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.__objs
weigs=self.__weights
if(Above^Below):#有一值为真
if(obj_move in weigs and obj_target in weigs):#确保俩控件都在画布中
obj_m=obj_move
obj_t=obj_target
weig_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_t
obj_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.__weights
def 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 True
def Set_ObjectPosition(self,obj,pos):#设置控件位置(pos为QRect)
if(obj not in self.__weights):
return False
if(not isinstance(pos,QRect)):#不是QRect,抛出错误(趁早修改错误调用)
raise TypeError("非QRect对象",pos)
self.__poses[obj]=pos
self.__Update(obj)
return True
def __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()/1000
if(self.__matrix[0][0]<0.05 and rate<1):#防止过度缩小
return
self.__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.__mouseStatus
ms.Opt_Update(event)#更新鼠标状态
def mouseReleaseEvent(self,event):#对象的点击事件在鼠标抬起时触发,而不在鼠标按下时触发,这样做是为了避免和拖拽操作相冲突
ms=self.__mouseStatus
ms.Opt_Update(event)#更新鼠标状态
if(not ms.Get_HasMoved()):#鼠标未发生拖拽行为
event.ignore()#让Object对象处理点击释放操作
def mouseMoveEvent(self,event):
ms=self.__mouseStatus
ms.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 sys
from PyQt5.QtWidgets import QApplication,QWidget,QLabel,QLineEdit,QPushButton
from XJ_Object import *
app = QApplication(sys.argv)
class Test(XJ_Object,QLabel):#需要继承XJ_Object。虽然不继承也没啥,一样能往XJ_Canvas塞Qt原生控件,就是点到控件时没法拖拽画布而已
# class Test(Object,QPushButton):
pass
cv= 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.MouseButtonRelease
self.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']=data
record['currMouse']=data
record['lastPress']=data
self.__record=record
def timerEvent(self,event):
record=self.__record
press=self.__press
tId=event.timerId()
cId=self.__timerID
self.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=interval
def Set_LongPressInterval(self,interval):#设置长按时间间隔(ms)
self.__longPressInterval=interval
def Set_AntiJitter(self,val):#设置防抖值
self.__antiJitter=val if val>0 else 0
def Get_Position(self):#返回鼠标坐标。是屏幕坐标(global),需要使用QWidget.mapFromGlobal(QPoint)自行转换为控件相对坐标
return self.__record['currMouse'].pos
def Get_PressButtonStatus(self):#返回当前鼠标的键(左中右)以及按下状态(单击/双击/抬起)
return self.__record['currMouse'].btn,self.__record['currMouse'].pressStatus
def Get_MoveDelta(self,total=True,strict=True):#返回鼠标移动量(仅鼠标按下时有效),为QPoint对象
press=self.__press
record=self.__record
data_curr=record['currMouse']
if(data_curr.pressStatus!=press[0]):#说明鼠标按下
if(not strict or self.__move):#严格模式下,仅判定发生移动时计算移动量
p1=record['currMouse'].pos
if(total):
p2=record['lastPress'].pos
else:
p2=record['lastMouse'].pos
return QPoint(p1.x()-p2.x(),p1.y()-p2.y())
return QPoint(0,0)
def Get_HasMoved(self):#判断是否发生移动(毕竟用Get_MoveDelta来判断移动的发生是有点麻烦,还不如多一个函数
return self.__move
def Opt_Update(self,event):#更新状态
press=self.__press
record=self.__record
data_curr=self.__Data(event)
if(event.type()==press[1] or event.type()==press[2]):#单/双击
self.__move=False
data_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_curr
record['lastMouse']=data_curr
record['currMouse']=data_curr
self.__timerID=self.startTimer(self.__longPressInterval)
else:#移动/抬起
data_curr.btn=event.buttons()
data_curr.pressStatus=record['lastMouse'].pressStatus
if(event.type()==press[0]):#抬起
if(data_curr.btn==Qt.NoButton):#确保无按键按下时设置为Release
data_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=True
record['currMouse'].pos=record['lastPress'].pos
record['lastMouse']=record['currMouse']
record['currMouse']=data_curr
if __name__=='__main__':
import sys
from PyQt5.QtWidgets import QApplication,QWidget
class Test(QWidget):
__mouseStatus=None
def __init__(self,*arg):
super().__init__(*arg)
ms=XJ_MouseStatus()
ms.longClick.connect(lambda:print("<LongClick!>"))
self.__mouseStatus=ms
def __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.__mouseStatus
pos=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):
pass
cv= 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:https://blog.csdn.net/weixin_44733774/article/details/134356809