开源五线谱打谱软件MuseScore代码分析之三:MuseScore的排版与绘制

时间:2024-04-15 21:01:12

 

MuseScore数据更新后,执行2个步骤,1:排版,2:绘制,把谱子重新画在屏幕上。
都有哪些操作会导致数据的更新?不仅包括鼠标,midi录制等编辑动作,打开一个新文件,界面下方的快速浏览条,打印也需要同样的排版绘图操作。打印预览就是在一块小些的空间内绘制和谱子完全一样的东西,所以使用相同的图形引擎,打印和打印预览的区别仅在输出设备上,这在Qt里已经提供了和硬件无关的接口,实现很方便。
排版:
Score::doLayout() 函数里是排版的动作,它包含如下步骤:
- 判断一种特殊情况:如果 _staves 为空,则建立一个空白页面,画这个空白页。
- 执行 3 个排版阶 段:
 1、算 notehead,lines,accidentals
 2、算 beams,确定 stem 方向(针对那些stem未指定的)
 3、算 notehead 水平方向的位置。
- 为 measure 分配 system 和 page,前面阶段完成后,小节所需要的空间可以确定,system 指页排版方式中一行包含的东西,page指一个页面中包含的东西,他们2个算是临时数据。
- 定位全局元素,如hairpin,tie,slur,beam,这些会跨小节的元素没有放在measure中,而是放在 Score 中,比如 Beam 放在 _beams 里,另外几个放在 _gel 里。
- 最后移除多余的 page 和 system,有时编辑操作后小节需要的空间会变小,这样就不需要那么多“页面”或“行”来显示。
绘制:
排版结束后进行绘制。每个Score都放在父容器ScoreView里,ScoreView 就是MDI程序中的一个文档窗口,系统绘制要求,或Qt的update(),updateAll()等都可以导致ScoreView::paintEvent(QPaintEvent* ev)被调用。
在ScoreView::paintEvent(QPaintEvent* ev)里得到更新区域QRegion,它由一组QRect组成,对每一个QRect,得到一个对应的元素列表,然后绘制每一个元素Element。
这里有一个关键的数据结构BspTree,用于快速的从屏幕坐标查找到Element。BspTree全名叫二叉空间分割树,是游戏开发中常用的数据结构。一棵层高为 n 的bsptree树可以容纳2^n个节点,其中只有叶子节点放的是真正的元素(Element),所以能容纳2^(n-1)个元素(Element)。所有父节点都做辅助作用,父节点对应一块所有子元素组成的区域,在查找时,判断point或rect是否在父节点的区域内,如果在,则到子树里去找,这样查找一个元素需要的平均时间为 n 次比较。该树在排版时建立,后面只要数据没变,树就固定,因此大大加快了元素定位的速度,是MuseScore程序运行飞快的主要功臣。
关于bsptree,可以到这里玩一下:
http://www.symbolcraft.com/graphics/bsp/index.php
看概念看的头晕,玩了这个才明白点了。mscore的空间分割没这么复杂,就是横一刀竖一刀的切成了一堆方块。
MuseScore的大量操作依赖bsptree的定位能力,鼠标操作,包括select,drag & drop等大量编辑操作,都是已知屏幕坐标位置,查找对应Element,再执行 Element 的相关函数。

MuseScore数据更新后,执行2个步骤,1:排版,2:绘制,把谱子重新画在屏幕上。

都有哪些操作会导致数据的更新?不仅包括鼠标,midi录制等编辑动作,打开一个新文件,界面下方的快速浏览条,打印也需要同样的排版绘图操作。打印预览就是在一块小些的空间内绘制和谱子完全一样的东西,所以使用相同的图形引擎,打印和打印预览的区别仅在输出设备上,这在Qt里已经提供了和硬件无关的接口,实现很方便。

  • 排版

Score::doLayout() 函数里是排版的动作,它包含如下步骤:
- 判断一种特殊情况:如果 _staves 为空,则建立一个空白页面,画这个空白页。
- 执行 3 个排版阶段: 
  1、算 notehead,lines,accidentals
   2、算 beams,确定 stem 方向(针对那些stem未指定的)
   3、算 notehead 水平方向的位置。
- 为 measure 分配 system 和 page,前面阶段完成后,小节所需要的空间可以确定,system 指页排版方式中一行包含的东西,page指一个页面中包含的东西,他们2个算是临时数据。
- 定位全局元素,如hairpin,tie,slur,beam,这些会跨小节的元素没有放在measure中,而是放在 Score 中,比如 Beam 放在 _beams 里,另外几个放在 _gel 里。
- 最后移除多余的 page 和 system,有时编辑操作后小节需要的空间会变小,这样就不需要那么多“页面”或“行”来显示。

  • 绘制

排版结束后进行绘制。每个Score都放在父容器ScoreView里,ScoreView 就是MDI程序中的一个文档窗口,系统绘制要求,或Qt的update(),updateAll()等都可以导致ScoreView::paintEvent(QPaintEvent* ev)被调用。在ScoreView::paintEvent(QPaintEvent* ev)里得到更新区域QRegion,它由一组QRect组成,对每一个QRect,得到一个对应的元素列表,然后绘制每一个元素Element。

这里有一个关键的数据结构BspTree,用于快速的从屏幕坐标查找到Element。
BspTree全名叫二叉空间分割树,是游戏开发中常用的数据结构。一棵层高为 n 的bsptree树可以容纳2^n个节点,其中只有叶子节点放的是真正的元素(Element),所以能容纳2^(n-1)个元素(Element)。所有父节点都做辅助作用,父节点对应一块所有子元素组成的区域,在查找时,判断point或rect是否在父节点的区域内,如果在,则到子树里去找,这样查找一个元素需要的平均时间为 n 次比较。该树在排版时建立,后面只要数据没变,树就固定,因此大大加快了元素定位的速度,是MuseScore程序运行飞快的主要功臣。关于bsptree,可以到这里玩一下:http://www.symbolcraft.com/graphics/bsp/index.php
看概念看的头晕,玩了这个才明白点了。mscore的空间分割没这么复杂,就是横一刀竖一刀的切成了一堆方块。MuseScore的大量操作依赖bsptree的定位能力,鼠标操作,包括select,drag & drop等大量编辑操作,都是已知屏幕坐标位置,查找对应Element,再执行 Element 的相关函数。