【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:https://blog.csdn.net/weixin_44733774/article/details/134356809

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/168958.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

编程艺术之源:深入了解设计模式和设计原则

深入了解设计模式和设计原则 一、认识设计模式1.1、设计模式是什么&#xff1f;1.2、设计模式是怎么来的&#xff1f;1.3、设计模式解决了什么问题&#xff1f; 二、设计模式的基础2.1、面向对象思想2.2、设计原则 三、如何学习设计模式3.1、明确目的3.2、学习步骤 总结 一、认…

『Linux升级路』基础开发工具——vim篇

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;Linux &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、vim的基本概念 &#x1f4d2;1.1命令模式 &#x1f4d2;1.2插入模式 &…

MCSM面板搭建教程和我的世界Paper服务器开服教程

雨云游戏云VPS服务器用Linux搭建MCSM面板和Minecraft Paper1.20.2服务器教程。 本教程演示安装的MC服是Paper 1.20.2版&#xff0c;其他版本也可以参考本教程&#xff0c;差别不大。 本教程使用Docker来运行mc服&#xff0c;可以方便切换不同Java版本&#xff0c;方便安装多个…

Haproxy实现七层负载均衡

目录 Haproxy概述 haproxy算法&#xff1a; Haproxy实现七层负载 ①部署nginx-server测试页面 ②(主/备)部署负载均衡器 ③部署keepalived高可用 ④增加对haproxy健康检查 ⑤测试 Haproxy概述 haproxy---主要是做负载均衡的7层&#xff0c;也可以做4层负载均衡 apache也可…

Bytebase 2.11.0 - 支持 OceanBase Oracle 模式

&#x1f680; 新功能 支持 OceanBase Oracle 模式。支持设置 MySQL 在线变更参数。新增项目数据库查看者的角色。 &#x1f384; 改进 支持在项目中直接选择所有用户并为之添加角色。 调整了项目页面的布局。在 SQL 编辑器中通过悬浮面板展示表和列的详情。 &#x1faa6; …

【Git】Git使用Gui图形化界面,Git中SSH协议,Idea集成Git

一&#xff0c;Git使用Gui图形化界面 1.1 Gui的简介 Gui &#xff08;Graphical User Interface&#xff09;指的是图形用户界面&#xff0c;也就是指使用图形化方式来协同人和计算机进行交互的一类程序。它与传统的命令行界面相比&#xff0c;更加直观、易用&#xff0c;用户…

数据结构之双向链表

目录 引言 链表的分类 双向链表的结构 双向链表的实现 定义 创建新节点 初始化 打印 尾插 头插 判断链表是否为空 尾删 头删 查找与修改 指定插入 指定删除 销毁 顺序表和双向链表的优缺点分析 源代码 dlist.h dlist.c test.c 引言 数据结构…

Linux shell编程学习笔记24:函数定义和使用

为了实现模块化设计和代码重用&#xff0c;很多编程语言支持函数或过程&#xff0c;Linux shell也支持函数定义和调用。 Linux shell中的函数与其它编程语言很多有相似之处&#xff0c;也有自己独特之处。 1 函数的定义 1.1 标准格式 function 函数名(){语句或命令1……语句…

基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖(三)

员工分页查询和账号启用禁用功能 1. 员工分页查询1.1 需求分析和设计1.1.1 产品原型1.1.2 接口设计 1.2 代码开发1.2.1 设计DTO类1.2.2 封装PageResult1.2.3 Controller层1.2.4 Service层接口1.2.5 Service层实现类1.2.6 Mapper层 1.3 功能测试1.4 代码完善 2. 启用禁用员工账号…

基于ubuntu22.04手动安装openstack——2023.2版本(最新版)的问题汇总

前言&#xff1a;基本上按照openstack官方网站动手可以搭建成功&#xff08;如有需要私信发部署文档&#xff09;。 但是任然有些小问题&#xff0c;所以汇总如下。 第一个问题 问题&#xff1a; ubuntu搭建2023.2版本neutorn报错&#xff0c;ERROR neutron.plugins.ml2.driv…

c++求三个数的最小公倍数

答案&#xff1a; #include <iostream> using namespace std; int main() {int n1, n2, n3, max;cin >> n1 >> n2 >> n3;max (n1 > n2 > n3) ? n1 : n2;do{if (max % n1 0 && max % n2 0 && max % n3 0){cout << ma…

2.docker镜像的导入导出

目录 概述docker 常用命令下载导出导入镜像结束 概述 docker 常用命令 本章节使用到的命令&#xff0c;总结在此&#xff0c;后面有使用案例。 命令作用docker images显示镜像docker rmi $(docker images -q)删除系统上所有的镜像docker rmi -f强制删除多个镜像 &#xff1a…