在之前的章节中,笔者一般使用qlabel控件来显示图片。但是,如果要使用很多图片怎么办?难道要实例化很多个qlabel控件来一一显示?那如何管理呢?当然,我们不可能会用qlabel控件来做这样的事,否则会非常麻烦和混乱。pyqt5中的图形视图可以让我们管理大量的自定义2d图元并与之交互。该框架使用bsp(binary space partitioning – 二叉空间分割)树,以快速查找图形元素。所以就算一个视图场景中包含数百万的图元,它也可以实时进行显示。如果要用pyqt5来制作稍微复杂点的游戏的话,图形视图是必定要用到的。
图形视图框架主要包含三个类:qgraphicsitem图元类、qgraphicsscene场景类和qgraphicsview视图类。简单一句话来概括下三者的关系就是:图元放在场景上,场景内容通过视图来显示。下面我们来一一进行讲解。
1.qgraphicsitem图元类
图元可以是文本、图片,规则几何图形或者任意自定义图形。该类已经提供了一些标准的图元,比如:
- 直线图元qgraphicslineitem
- 矩形图元qgraphicsrectitem
- 椭圆图元qgraphicsellipseitem
- 图片图元qgraphicspixmapitem
- 文本图元qgraphicstextitem
- 路径图元qgraphicspathitem
想必通过名称也可以知道这些图元是用来干嘛的,我们通过以下代码来演示如何使用:
import sys from pyqt5.qtcore import qt from pyqt5.qtgui import qpixmap, qcolor, qpainterpath from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicslineitem, qgraphicsrectitem, qgraphicsellipseitem, qgraphicspixmapitem, qgraphicstextitem, qgraphicspathitem, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() # 1 self.resize(300, 300) # 2 self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) # 3 self.line = qgraphicslineitem() self.line.setline(100, 10, 200, 10) # self.line.setline(qlinef(100, 10, 200, 10)) # 4 self.rect = qgraphicsrectitem() self.rect.setrect(100, 30, 100, 30) # self.rect.setrect(qrectf(100, 30, 100, 30)) # 5 self.ellipse = qgraphicsellipseitem() self.ellipse.setrect(100, 80, 100, 20) # self.ellipse.setrect(qrectf(100, 80, 100, 20)) # 6 self.pic = qgraphicspixmapitem() self.pic.setpixmap(qpixmap('pic.png').scaled(60, 60)) self.pic.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.pic.setoffset(100, 120) # self.pic.setoffset(qpointf(100, 120)) # 7 self.text1 = qgraphicstextitem() self.text1.setplaintext('hello pyqt5') self.text1.setdefaulttextcolor(qcolor(66, 222, 88)) self.text1.setpos(100, 180) self.text2 = qgraphicstextitem() self.text2.setplaintext('hello world') self.text2.settextinteractionflags(qt.texteditorinteraction) self.text2.setpos(100, 200) self.text3 = qgraphicstextitem() self.text3.sethtml('<a href="https://baidu.com" rel="external nofollow" >百度</a>') self.text3.setopenexternallinks(true) self.text3.settextinteractionflags(qt.textbrowserinteraction) self.text3.setpos(100, 220) # 8 self.path = qgraphicspathitem() self.tri_path = qpainterpath() self.tri_path.moveto(100, 250) self.tri_path.lineto(130, 290) self.tri_path.lineto(100, 290) self.tri_path.lineto(100, 250) self.tri_path.closesubpath() self.path.setpath(self.tri_path) # 9 self.scene.additem(self.line) self.scene.additem(self.rect) self.scene.additem(self.ellipse) self.scene.additem(self.pic) self.scene.additem(self.text1) self.scene.additem(self.text2) self.scene.additem(self.text3) self.scene.additem(self.path) # 10 self.setscene(self.scene) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 该类直接继承qgraphicsview,那么窗口就是视图,且大小为300×300;
2. 实例化一个qgraphicsscene场景,并调用setscenerect(x, y, w, h)方法来设置场景坐标原点和大小。从代码中我们得知坐标原点为(0, 0),之后往场景中添加的图元就会都根据该坐标来设置位置(关于坐标的更多内容,笔者会在34.4小节中进行讲解)。场景的大小为300×300,跟视图大小一样;
3. 实例化一个qgraphicslineitem直线图元,并调用setline()方法设置直线两端的坐标。该方法既可以直接传入四个数值,也可以传入一个qlinef对象。文档里写的非常清楚:
4-5. 跟直线图元类似,这里分别实例化矩形图元和椭圆图元,并调用相应的方法来设置位置和大小;
6. 实例化一个图片图元,并调用setpixmap()方法设置图片,qpixmap对象有个scaled()方法可以设置图片的大小(当然我们也可以使用qgraphicsitem的setscale()方法来设置),接着我们设置该图元的flag属性,让他可以被选中以及移动,这是所有图元共有的方法。最后调用setoffset()方法来设置图片相对于场景坐标原点的偏移量;
7. 这里实例化了三个文本图元,分别显示普通绿色文本,可编辑文本以及超链接文本(html)。setdefaultcolor()方法可以用来设置文本的颜色,setpos()用来设置文本图元相对于场景坐标原点的位置(该方法是所有图元共有的方法,我们当然也可以使用在其他类型的图元上)。
settextinteractionflags()用来设置文本属性,这里的qt.texteditorinteraction参数表示为可编辑属性(相当于在qtextedit上编辑文本),最后的qt.textbrowserinteraction表明该文本用于浏览(相当于在qtextbrowser上的文本)。有关更多的属性,大家可以在文档里搜索qt::textinteractionflags来了解。
当然如果要让超链接文本能够被打开,我们还需要使用setopenexternallinks()方法,传入一个true参数即可。
8. 路径图元可以用于显示任意形状的图形,setpath()方法需要传入一个qpainterpath对象,而我们就是用该对象来进行绘画操作的。moveto()方法表示将画笔移动到相应位置上,lineto()表示画一条直线,closesubpath()方法表示当前作画结束 (查阅文档来了解更多有关qpaintpath对象的方法),这里我们画了一个直角三角形;
9. 调用场景的additem()方法将所有图元添加进来;
10. 调用setscene()方法来让场景居中显示在视图中。
运行截图如下:
图片可以被选中和移动:
hello world文本可以被编辑:
qgraphicsitem还支持以下特性:
- 鼠标按下、移动、释放和双击事件,以及鼠标悬浮事件、滚轮事件和右键菜单事件
- 键盘输入事件
- 拖放事件
- 分组
- 碰撞检测
实现事件函数非常简单,这里就不细讲,我们重点要来了解下它在图形视图框架中的是如何传递的。请看下面的代码:
import sys from pyqt5.qtwidgets import qapplication, qgraphicsrectitem, qgraphicsscene, qgraphicsview class customitem(qgraphicsrectitem): def __init__(self): super(customitem, self).__init__() self.setrect(100, 30, 100, 30) def mousepressevent(self, event): print('event from qgraphicsitem') super().mousepressevent(event) class customscene(qgraphicsscene): def __init__(self): super(customscene, self).__init__() self.setscenerect(0, 0, 300, 300) def mousepressevent(self, event): print('event from qgraphicsscene') super().mousepressevent(event) class customview(qgraphicsview): def __init__(self): super(customview, self).__init__() self.resize(300, 300) def mousepressevent(self, event): print('event from qgraphicsview') super().mousepressevent(event) if __name__ == '__main__': app = qapplication(sys.argv) view = customview() scene = customscene() item = customitem() scene.additem(item) view.setscene(scene) view.show() sys.exit(app.exec_())
图元,场景和视图其实都有各自的事件函数,我们在上面分别继承了qgraphicsrectitem, qgraphicsscene以及qgraphicsview并重新实现了各自的mousepressevent()事件函数,在其中我们都打印一句话来让用户知道是哪个函数被执行了。
运行截图如下:
我们在矩形框内点击之后,发现控制台输入如下信息:
由此可见,事件的传递顺序为视图->场景->图元。有一点大家需要注意,重新实现事件函数的话我们必须要调用相应的父类事件函数,否则事件无法顺利传递下去。假如我把customview类中事件函数下的super().mousepressevent(event)这行代码删除掉,那么控制台只会输出"event from qgraphicsview":
一个图元中可以添加另一个图元(一个图元可以是另一个图元的父类),那此时图元之间的事件传递顺序又是如何的呢?请看下面代码:
import sys from pyqt5.qtwidgets import qapplication, qgraphicsrectitem, qgraphicsscene, qgraphicsview class customitem(qgraphicsrectitem): def __init__(self, num): super(customitem, self).__init__() self.setrect(100, 30, 100, 30) self.num = num def mousepressevent(self, event): print('event from qgraphicsitem{}'.format(self.num)) super().mousepressevent(event) if __name__ == '__main__': app = qapplication(sys.argv) view = qgraphicsview() scene = qgraphicsscene() item1 = customitem(1) item2 = customitem(2) item2.setparentitem(item1) scene.additem(item1) view.setscene(scene) view.show() sys.exit(app.exec_())
因为实例化的是两个一样的矩形图源,为了进行区分,我们在customitem的初始化函数中加入一个num参数,然后在事件函数中打印出实例化时所传入的数字即可。
调用setparentitem()方法将item1设置为item2的父类,然后将item1添加到场景中(item2自然也被加入)。
运行截图如下:
在矩形框中点击,控制台打印如下:
由此可见,事件是由子图元传递到父图元的。同理,如果不加super().mousepressevent(event),那么事件就会停止传递,最后也就只会显示"event from qgraphicsitem2":
请大家一定要搞清楚事件的传递顺序,这样才能更好地使用图形视图框架。
所谓分组也就是将各个图元进行分类,分到一起的图元就会共同行动(选中、移动以及复制等)。我们通过下面的代码来演示下:
import sys from pyqt5.qtcore import qt from pyqt5.qtgui import qpen, qbrush from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicsrectitem, qgraphicsellipseitem, qgraphicsscene, qgraphicsview, qgraphicsitemgroup class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) # 1 self.rect1 = qgraphicsrectitem() self.rect2 = qgraphicsrectitem() self.ellipse1 = qgraphicsellipseitem() self.ellipse2 = qgraphicsellipseitem() self.rect1.setrect(100, 30, 100, 30) self.rect2.setrect(100, 80, 100, 30) self.ellipse1.setrect(100, 140, 100, 20) self.ellipse2.setrect(100, 180, 100, 50) # 2 pen1 = qpen(qt.solidline) pen1.setcolor(qt.blue) pen1.setwidth(3) pen2 = qpen(qt.dashline) pen2.setcolor(qt.red) pen2.setwidth(2) brush1 = qbrush(qt.solidpattern) brush1.setcolor(qt.blue) brush2 = qbrush(qt.solidpattern) brush2.setcolor(qt.red) self.rect1.setpen(pen1) self.rect1.setbrush(brush1) self.rect2.setpen(pen2) self.rect2.setbrush(brush2) self.ellipse1.setpen(pen1) self.ellipse1.setbrush(brush1) self.ellipse2.setpen(pen2) self.ellipse2.setbrush(brush2) # 3 self.group1 = qgraphicsitemgroup() self.group2 = qgraphicsitemgroup() self.group1.addtogroup(self.rect1) self.group1.addtogroup(self.ellipse1) self.group2.addtogroup(self.rect2) self.group2.addtogroup(self.ellipse2) self.group1.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.group2.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) print(self.group1.boundingrect()) print(self.group2.boundingrect()) # 4 self.scene.additem(self.group1) self.scene.additem(self.group2) self.setscene(self.scene) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 实例化四个图元,两个为矩形,两个为椭圆,并调用setrect()方法设置坐标和大小;
2. 实例化两种画笔和两种画刷,用于图元的样式设置;
3. 实例化两个qgraphicsgroup分组对象,并将矩形和椭圆都添加进来。rect1和ellipse1在group1里,而rect2和ellipse2在group2里。接着调用setflags()方法设置属性,让分组可以选中和移动。boundrect()方法放回一个qrectf值,该值可以显示出分组的边界位置和大小;
4. 将分组添加到场景当中。
运行截图如下:
蓝色的矩形和椭圆为一组,可同时选中和移动,红色的同理。黑色边框即为边界,其位置和大小可用boundrect()方法来获取。通过下面的截图我们可以发现qgraphicsitemgroup的边界的位置和大小由其中的图元整体所决定:
碰撞检测在游戏中的用处非常大,比如在飞机大战游戏中,如果子弹没有和敌机做碰撞检测处理的话,那敌机就不会被消灭,奖励也不会增加,游戏也就没有什么意思。我们通过下面这个例子来带大家了解如何对图元进行碰撞检测:
界面上有一个矩形图元和一个椭圆图元,两者都可以选中和移动。我们就对两者进行碰撞检测。在此之前我们先了解下boundingrect()边界和shape()形状的区别。请看下方的椭圆图元:
当选中这个图元时,虚线部分显示的就是该图元的边界,而形状就指的是图元本身,也就是黑色实线部分。碰撞检测可以以边界为范围或者以形状为范围。假如我们在代码中以边界为范围,那椭圆的虚线跟矩形图元一碰到,就会触发碰撞检测;如果以形状为范围,那只有在椭圆的黑色实线跟矩形碰到的情况下,碰撞检测才会触发。
下面是几种具体的检测方式:
下面请看代码示例:
import sys from pyqt5.qtcore import qt from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicsrectitem, qgraphicsellipseitem, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) self.rect = qgraphicsrectitem() self.ellipse = qgraphicsellipseitem() self.rect.setrect(120, 30, 50, 30) self.ellipse.setrect(100, 180, 100, 50) self.rect.setflags(qgraphicsitem.itemismovable | qgraphicsitem.itemisselectable) self.ellipse.setflags(qgraphicsitem.itemismovable | qgraphicsitem.itemisselectable) self.scene.additem(self.rect) self.scene.additem(self.ellipse) self.setscene(self.scene) def mousemoveevent(self, event): if self.ellipse.collideswithitem(self.rect, qt.intersectsitemboundingrect): print(self.ellipse.collidingitems(qt.intersectsitemshape)) super().mousemoveevent(event) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
初始化函数中的代码想必大家都懂了,这里就不再讲述,我们重点来看mousemoveevent()事件函数。
我们调用椭圆图元的collideswithitem()方法来指定要与之进行碰撞检测的其他图元以及检测方式。其他图元指的就是矩形图元,而且我们可以看到这里是以椭圆的边界为范围,而且只要两个图元有交集就会触发检测。如果碰撞条件成立,那么collideswithitem()就会返回一个true,那么此时if条件判断也就成立。
collidingitems()方法在指定检测方式后可以返回所有符合碰撞条件的其他图元,返回值类型为列表。这里的检测方式是以形状为范围的,同样有交集即可。
那mousemoveevent()事件函数所要表达的意思就是:当椭圆的边界和矩形接触,那么if条件判断就成立,不过此时打印的还只是空列表,因为椭圆本身(黑色实线)并还没有跟矩形有所接触。不过当接触了之后控制台就会输出包含矩形图元的列表了。
请大家调用矩形图元的collideswithitem()和collidingitems()方法来尝试下,看看有什么不同。也就是把mousemoveevent()事件函数修改如下:
def mousemoveevent(self, event): if self.rect.collideswithitem(self.ellipse, qt.intersectsitemboundingrect): print(self.rect.collidingitems(qt.intersectsitemshape)) super().mousemoveevent(event)
出于性能考虑,qgraphicsitem不继承自qobject,所以本身并不能使用信号和槽机制,我们也无法给它添加动画。不过我们可以自定义一个类,并让该类继承自qgraphicsobject。请看下面的解决方案:
import sys from pyqt5.qtcore import qpropertyanimation, qpointf, qrectf, pyqtsignal from pyqt5.qtwidgets import qapplication, qgraphicsscene, qgraphicsview, qgraphicsobject class customrect(qgraphicsobject): # 1 my_signal = pyqtsignal() def __init__(self): super(customrect, self).__init__() # 2 def boundingrect(self): return qrectf(0, 0, 100, 30) # 3 def paint(self, painter, styles, widget=none): painter.drawrect(self.boundingrect()) class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) # 4 self.rect = customrect() self.rect.my_signal.connect(lambda: print('signal and slot')) self.rect.my_signal.emit() self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) self.scene.additem(self.rect) self.setscene(self.scene) # 5 self.animation = qpropertyanimation(self.rect, b'pos') self.animation.setduration(3000) self.animation.setstartvalue(qpointf(100, 30)) self.animation.setendvalue(qpointf(100, 200)) self.animation.setloopcount(-1) self.animation.start() if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 自定义一个信号;
2-3. 继承qgraphicsobject的话,我们最好把boundingrect()和paint()方法重新实现下。在boundingrect()中我们返回一个qrectf类型值来确定customrect的默认位置和大小。在paint()中调用drawrect()方法将矩形画到界面上;
4. 将自定义的信号和槽函数连接,槽函数中打印“signal and slot”字符串。接着调用信号的emit()方法来发射信号,那么槽函数也就会启动了;
5. 加上qpropertyanimation属性动画,将矩形从(100, 30)移动到(100, 200),时间为3秒,动画无限循环。
运行截图如下,矩形图元从上而下缓缓移动:
控制台打印内容:
2.qgraphicsscene场景类
在之前的小节中,我们要往场景中添加图元的话都是先把图元实例化好,再调用场景的additem()方法进行添加。不过场景其实还提供了以下方法让我们可以快速添加图元:
当然场景还提供了很多用于管理图元的方法。我们通过下面的代码来学习下:
import sys from pyqt5.qtcore import qt from pyqt5.qtgui import qpixmap, qtransform from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) # 1 self.rect = self.scene.addrect(100, 30, 100, 30) self.ellipse = self.scene.addellipse(100, 80, 50, 40) self.pic = self.scene.addpixmap(qpixmap('pic.png').scaled(60, 60)) self.pic.setoffset(100, 130) self.rect.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable | qgraphicsitem.itemisfocusable) self.ellipse.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable | qgraphicsitem.itemisfocusable) self.pic.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable | qgraphicsitem.itemisfocusable) self.setscene(self.scene) # 2 print(self.scene.items()) print(self.scene.items(order=qt.ascendingorder)) print(self.scene.itemsboundingrect()) print(self.scene.itemat(110, 40, qtransform())) # 3 self.scene.focusitemchanged.connect(self.my_slot) def my_slot(self, new_item, old_item): print('new item: {}nold item: {}'.format(new_item, old_item)) # 4 def mousemoveevent(self, event): print(self.scene.collidingitems(self.ellipse, qt.intersectsitemshape)) super().mousemoveevent(event) # 5 还需要修改 def mousedoubleclickevent(self, event): item = self.scene.itemat(event.pos(), qtransform()) self.scene.removeitem(item) super().mousedoubleclickevent(event) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 直接调用场景的addrect(), addellipse()和addpixmap()方法来添加图元。这里需要大家了解一个知识点:先添加的图元处于后添加的图元下方(z轴方向),大家可以自己运行下代码然后移动下图元,之后就会发现该程序中图片图元处于最上方,椭圆其次,而矩形处于最下方。不过我们可以通过调用图元的setzvalue()方法来改变上下位置(请查阅文档来了解,这里不详细解释)。
接着设置图元的flag属性。这里多出来的一个itemisfocusable表示让图元可以聚焦(默认是无法聚焦的),该属性跟下面第3小点中要讲的foucsitemchanged信号有关;
2. 调用items()方法可以返回场景中的所有图元,返回值类型为列表。返回的元素默认以降序方式(qt.descendingorder),也就是从上到下进行排列(qpixmapitem, qellipseitem, qrectitem)。可修改order参数的值,让列表中返回的元素按照升序方式排列。
itemsboundingrect()返回所有图元所构成的整体的边界。
itemat()可以返回指定位置上的图元,如果在这个位置上有两个重叠的图元的话,那就返回最上面的图元,传入的qtransform()跟图元的flag属性itemignorestransformations有关,由于这里没有设置该属性我们直接传入qtransform()就行(这里不细讲,否则可能就会比较混乱了,大家可以先单纯记住,之后再深入研究);
3. 场景有个focuschangeditem信号,当我们选中不同的图元时,该信号就会发出,前提是图元设置了itemisfocusable属性。该信号可以传递两个值过来,第一个是新选中的图元,第二个是之前选中的图元;
4. 调用场景的collidingitems()可以打印出在指定碰撞触发条件下,所有和目标图元发生碰撞的其他图元;
5. 我们在图元上双击下,就可以调用removeitem()方法将其删除。注意这里其实直接给itemat()传入event.pos()是不准确的,因为event.pos()其实是鼠标在视图上的坐标而不是场景上的坐标。大家可以把窗口放大,然后再双击试下,会发现图元并不会消失,这是因为视图大小跟场景大小不再一样,坐标也发生了改变。具体解决方案请看34.4小节。
运行截图如下:
控制台打印内容:
双击某个图元,将其删除:
我们还可以向场景中添加qlabel, qlineedit, qpushbutton, qtablewidget等简单或者复杂的控件,甚至可以直接添加一个主窗口。接下来通过完成以下界面来带大家进一步了解(就是第三章布局管理中的界面例子):
代码如下:
import sys from pyqt5.qtcore import qt from pyqt5.qtwidgets import qapplication, qgraphicsscene, qgraphicsview, qgraphicswidget, qgraphicsgridlayout, qgraphicslinearlayout, qlabel, qlineedit, qpushbutton class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(220, 110) # 1 self.user_label = qlabel('username:') self.pwd_label = qlabel('password:') self.user_line = qlineedit() self.pwd_line = qlineedit() self.login_btn = qpushbutton('log in') self.signin_btn = qpushbutton('sign in') # 2 self.scene = qgraphicsscene() self.user_label_proxy = self.scene.addwidget(self.user_label) self.pwd_label_proxy = self.scene.addwidget(self.pwd_label) self.user_line_proxy = self.scene.addwidget(self.user_line) self.pwd_line_proxy = self.scene.addwidget(self.pwd_line) self.login_btn_proxy = self.scene.addwidget(self.login_btn) self.signin_btn_proxy = self.scene.addwidget(self.signin_btn) print(type(self.user_label_proxy)) # 3 self.g_layout = qgraphicsgridlayout() self.l_h_layout = qgraphicslinearlayout() self.l_v_layout = qgraphicslinearlayout(qt.vertical) self.g_layout.additem(self.user_label_proxy, 0, 0, 1, 1) self.g_layout.additem(self.user_line_proxy, 0, 1, 1, 1) self.g_layout.additem(self.pwd_label_proxy, 1, 0, 1, 1) self.g_layout.additem(self.pwd_line_proxy, 1, 1, 1, 1) self.l_h_layout.additem(self.login_btn_proxy) self.l_h_layout.additem(self.signin_btn_proxy) self.l_v_layout.additem(self.g_layout) self.l_v_layout.additem(self.l_h_layout) # 4 self.widget = qgraphicswidget() self.widget.setlayout(self.l_v_layout) # 5 self.scene.additem(self.widget) self.setscene(self.scene) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 实例化需要的控件,因为父类不是qgraphicsview,所以不加self;
2. 实例化一个场景对象,然后调用addwidget()方法来添加控件。addwidget()方法返回的值其实是一个qgraphicsproxywidget代理对象,控件就是嵌入到该对象所提供的代理层中。user_label_proxy跟user_label的状态保持一致,如果我们禁用或者隐藏了user_label_proxy,那么相应的user_label也会被禁用或者隐藏掉,那我们就可以在场景中通过控制代理对象来操作控件(不过信号和槽还是要直接应用到控件上,代理对象不提供)。
3. 进行布局,注意这里用的是图形视图框架中的布局管理器:qgraphicsgridlayout网格布局和qgraphicslinearlayout线形布局(水平和垂直布局结合)。不过用法其实差不多,只不过调用的方法是additem()而不是addwidget()或者addlayout()了。线形布局默认是水平的,我们可以在实例化的时候传入qt.vertical来进行垂直布局(图形视图还有个锚布局qgraphicsanchorlayout,这里不再讲解,相信大家文档也可以看的明白);
4. 实例化一个qgraphicswidget,这个跟qwidget类似,只不过是用在图形视图框架这边,调用setlayout()方法来设置整体布局;
5. 将qgraphicswidget对象添加到场景中,qgraphicsproxywidget中嵌入的控件自然也就在场景上了,最后将场景显示在视图中就可以了。
3.qgraphicsview视图类
视图其实是一个滚动区域,如果视图小于场景大小的话,那窗口就会显示滚动条好让用户可以观察到全部场景(在linux和windows系统上,如果视图和场景大小一样,滚动条也会显示出来)。在下面的代码中,笔者让场景大于视图:
import sys from pyqt5.qtcore import qrectf from pyqt5.qtwidgets import qapplication, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 500, 500) self.scene.addellipse(qrectf(200, 200, 50, 50)) self.setscene(self.scene) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
视图大小为300×300,场景大小为500×500。
运行截图如下:
macos
linux(ubuntu)
windows
既然图元已经添加好,场景也已经设置好,那我们通常就可以调用视图的一些方法来对图元做一些变换,比如放大、缩小和旋转等。请看下方代码:
import sys from pyqt5.qtcore import qt, qrectf from pyqt5.qtgui import qcolor, qbrush from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(300, 300) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 500, 500) self.ellipse = self.scene.addellipse(qrectf(200, 200, 50, 50), brush=qbrush(qcolor(qt.blue))) self.rect = self.scene.addrect(qrectf(300, 300, 50, 50), brush=qbrush(qcolor(qt.red))) self.ellipse.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.rect.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.setscene(self.scene) self.press_x = none # 1 def wheelevent(self, event): if event.angledelta().y() < 0: self.scale(0.9, 0.9) else: self.scale(1.1, 1.1) # super().wheelevent(event) # 2 def mousepressevent(self, event): self.press_x = event.x() # super().mousepressevent(event) def mousemoveevent(self, event): if event.x() > self.press_x: self.rotate(10) else: self.rotate(-10) # super().mousemoveevent(event) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
1. 在鼠标滚轮事件中,调用scale()方法来来放大和缩小视图。这里并没有必要调用父类的事件函数,因为我们不需要将事件传递给场景以及图元;
2. 重新实现鼠标按下和移动事件函数,首先获取鼠标按下时的坐标,然后判断鼠标是向左移动还是向右。如果向右的话,则视图顺时针旋转10度,否则逆时针旋转10度。
运行截图如下:
放大和缩小
旋转
当然视图还提供了很多方法,比如同样可以用items()和itemat()来获取图元,也可以设置视图背景、视图图缓存模式和鼠标拖曳模式等等。大家可按需查阅(这里讲多了怕混乱(ー`´ー))。
4.图形视图的坐标体系
(更新) 图形视图基于笛卡尔坐标系,视图,场景和图元坐标系都一样——左上角为原点,向右为x正轴,向下为y正轴。
图形视图提供了三种坐标系之间相互转换的函数,以及图元与图元之间的转换函数:
好,我们现在来讲解下34.2小节中的那个问题,代码如下:
import sys from pyqt5.qtgui import qpixmap, qtransform from pyqt5.qtwidgets import qapplication, qgraphicsitem, qgraphicsscene, qgraphicsview class demo(qgraphicsview): def __init__(self): super(demo, self).__init__() self.resize(600, 600) self.scene = qgraphicsscene() self.scene.setscenerect(0, 0, 300, 300) self.rect = self.scene.addrect(100, 30, 100, 30) self.ellipse = self.scene.addellipse(100, 80, 50, 40) self.pic = self.scene.addpixmap(qpixmap('pic.png').scaled(60, 60)) self.pic.setoffset(100, 130) self.rect.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.ellipse.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.pic.setflags(qgraphicsitem.itemisselectable | qgraphicsitem.itemismovable) self.setscene(self.scene) def mousedoubleclickevent(self, event): item = self.scene.itemat(event.pos(), qtransform()) self.scene.removeitem(item) super().mousedoubleclickevent(event) if __name__ == '__main__': app = qapplication(sys.argv) demo = demo() demo.show() sys.exit(app.exec_())
在上面这个程序中,视图大小为600×600,而场景大小只有300×300。此时运行程序,我们双击的话是删除不了图元的,原因就是我们所获取的event.pos()是视图上的坐标,但是self.scene.itemat()需要的是场景坐标。把视图坐标传给场景的itemat()方法是获取不到任何图元的,所以我们应该要进行转换!
把mousedoubleclickevent()事件函数修改如下即可:
def mousedoubleclickevent(self, event): point = self.maptoscene(event.pos()) item = self.scene.itemat(point, qtransform()) self.scene.removeitem(item) super().mousedoubleclickevent(event)
调用视图的maptoscene()方法将视图坐标转换为场景坐标,这样图元就可以找到,也就自然而然可以删除掉了。
运行截图如下,椭圆被删除了:
5.小结
1. 事件的传递顺序为视图->场景->图元,如果是在图元父子类之间传递的话,那传递顺序是从子类到父类;
2. 碰撞检测的范围分为边界和形状两种,需要明白两者的不同;
3. 要给qgraphicsitem加上信号和槽机制以及动画的话,就自定义一个继承于qgraphicsobject的类;
4. 往场景中添加qlabel, qlineedit, qpushbutton等控件,我们需要用到qgraphicsproxywidget;
5. 视图,场景和图元都有自己的坐标系,注意使用坐标转换函数进行转换;
6. 图形视图框架知识点太多,笔者写本章的目的只是尽量带大家入门,个别地方可能会没有解释详细,请各位谅解。关于更多细节,大家可以在qt assistant中搜索“graphics view framework”来进一步了解。
以上就是深入了解pyqt5中的图形视图框架的详细内容,更多关于pyqt5图形视图框架的资料请关注<计算机技术网(www.ctvol.com)!!>其它相关文章!
需要了解更多c/c++开发分享深入了解PyQt5中的图形视图框架,都可以关注C/C++技术分享栏目—计算机技术网(www.ctvol.com)!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/1074125.html