主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数 |
画布控制
我们在画布上画饼形区域(或者条形图表条),和所有的标签。画布是通过画布视图来呈现给用户的。drawElements()函数被调用从而在需要的时候重新绘制画布。
(由chartform_canvas.cpp展开。)
drawElements()
void ChartForm::drawElements() { QCanvasItemList list = m_canvas->allItems(); for ( QCanvasItemList::iterator it = list.begin(); it != list.end(); ++it ) delete *it;
我们在drawElements()中所作的第一件事是删除所有已经存在的画布条目。
// 360 * 16为一个饼形,Qt中使用的是16倍的度数(就是它的一个圆周为360x16) int scaleFactor = m_chartType == PIE ? 5760 : m_chartType == VERTICAL_BAR ? m_canvas->height() : m_canvas->width();
接下来我们根据要绘制的图表的种类来计算比例因子。
double biggest = 0.0; int count = 0; double total = 0.0; static double scales[MAX_ELEMENTS]; for ( int i = 0; i < MAX_ELEMENTS; ++i ) { if ( m_elements[i].isValid() ) { double value = m_elements[i].value(); count++; total += value; if ( value > biggest ) biggest = value; scales[i] = m_elements[i].value() * scaleFactor; } } if ( count ) { // 第二个循环是因为总量和最大的 for ( int i = 0; i < MAX_ELEMENTS; ++i ) if ( m_elements[i].isValid() ) if ( m_chartType == PIE ) scales[i] = (m_elements[i].value() * scaleFactor) / total; else scales[i] = (m_elements[i].value() * scaleFactor) / biggest;
我们需要知道这里有多少值、最大的值和值的总和,这样我们就可以正确地按比例创建饼形区域或条形了。我们把比例值存放在scales数组中。
switch ( m_chartType ) { case PIE: drawPieChart( scales, total, count ); break; case VERTICAL_BAR: drawVerticalBarChart( scales, total, count ); break; case HORIZONTAL_BAR: drawHorizontalBarChart( scales, total, count ); break; } }
既然我们已经知道了必需的信息,那我们就调用相关绘制函数,传递比例值、总量和计数。
m_canvas->update();
最终我们使用update()更新画布来使所有的变化可视。
drawHorizontalBarChart()
我们来回顾一下刚才的这个绘制函数,看到了画布条目如何被生成并放置到画布上,因为这个教程是关于Qt的,而不是关于绘制图表的好的(或者坏的)算法。
void ChartForm::drawHorizontalBarChart( const double scales[], double total, int count ) {
画水平条形图我们需要一个比例值的数组、总量(这样我们就可以在需要的时候计算并且画出百分比)和这一组值的计数。
double width = m_canvas->width(); double height = m_canvas->height(); int proheight = int(height / count); int y = 0;
我们重新得到画布的宽度和高度并且计算比例高度(proheight)。我们把初始的y位置设为0。
QPen pen; pen.setStyle( NoPen );
我们创建一个用来绘制每一个条形(矩形)的画笔,我们把它设置为NoPen,这样就不会画出边框。
for ( int i = 0; i < MAX_ELEMENTS; ++i ) { if ( m_elements[i].isValid() ) { int extent = int(scales[i]);
我们在元素矢量中迭代每一个元素,忽略无效的元素。每个条的宽度(它的长度)很简单地就是它的比例值。
QCanvasRectangle *rect = new QCanvasRectangle( 0, y, extent, proheight, m_canvas ); rect->setBrush( QBrush( m_elements[i].valueColor(), BrushStyle(m_elements[i].valuePattern()) ) ); rect->setPen( pen ); rect->setZ( 0 ); rect->show();
我们为每个条形创建一个新的QCanvasRectangle,它的x位置为0(因为这是一个水平条形图,每个条形都从左边开始),y值从0开始,随着每一个要画的条形的高度增长,一直到我们要画的条形和画布的高度。然后我们设置条形的画刷为用户为元素指定的颜色和样式,设置画笔为我们先前生成的画笔(比如,设置为NoPen)并且我们把条形的Z轴顺序设置为0。最后我们调用show()在画布上绘制条形。
QString label = m_elements[i].label(); if ( !label.isEmpty() || m_addValues != NO ) { double proX = m_elements[i].proX( HORIZONTAL_BAR ); double proY = m_elements[i].proY( HORIZONTAL_BAR ); if ( proX < 0 || proY < 0 ) { proX = 0; proY = y / height; }
如果用户已经为元素指定了标签或者要求把值(或者百分比)显示出来,我们也要画一个画布文本条目。我们创建我们自己的CanvasText类(请看后面),因为我们想存储每一个画布文本条目中对应元素的索引(在元素矢量中)。我们从元素中得出x和y的比例值。如果其中之一< 0,那么他们还没有被用户定位,所以你必须计算它们的位置。我们标签的x值为0(左)并且y值为条形图的顶部(这样标签的左上角就会在x,y位置)。
label = valueLabel( label, m_elements[i].value(), total );
然后我们调用一个助手函数valueLabel(),它可以返回一个包含标签文本的字符串。(如果用户已经设置相应的选项,valueLabel()函数添加值或者百分比到这个文本的标签。)
CanvasText *text = new CanvasText( i, label, m_font, m_canvas ); text->setColor( m_elements[i].labelColor() ); text->setX( proX * width ); text->setY( proY * height ); text->setZ( 1 ); text->show(); m_elements[i].setProX( HORIZONTAL_BAR, proX ); m_elements[i].setProY( HORIZONTAL_BAR, proY );
然后我们创建一个CanvasText条目,传递给它在元素矢量中这个元素的索引和所要使用的标签、字体和画布。我们设置文本条目的颜色为用户指定的颜色并且设置条目的x和y位置和画布的宽高成比例。我们设置Z轴顺序为1,这样文本条目总是在条形(Z轴顺序为0)的上面(前面)。我们调用show()函数在画布上绘制文本条目,并且设置元素的相对x和y位置。
} y += proheight;
在绘制完条形和可能存在的标签之后,我们给y增加一定比例的高度用来准备绘制下一个元素。
} } }
QCanvasText的子类
(由canvastext.h展开。)
class CanvasText : public QCanvasText { public: enum { CANVAS_TEXT = 1100 }; CanvasText( int index, QCanvas *canvas ) : QCanvasText( canvas ), m_index( index ) {} CanvasText( int index, const QString& text, QCanvas *canvas ) : QCanvasText( text, canvas ), m_index( index ) {} CanvasText( int index, const QString& text, QFont font, QCanvas *canvas ) : QCanvasText( text, font, canvas ), m_index( index ) {} int index() const { return m_index; } void setIndex( int index ) { m_index = index; } int rtti() const { return CANVAS_TEXT; } private: int m_index; };
我们的CanvasText子类是QCanvasText的一个非常简单的特化。我们所做的一切只是添加一个私有成员m_index,它用来保存和这个文本相关的元素的元素矢量索引,并且提供为这个值提供一个读和写函数。
QCanvasView的子类
(由canvasview.h展开。)
class CanvasView : public QCanvasView { Q_OBJECT public: CanvasView( QCanvas *canvas, ElementVector *elements, QWidget* parent = 0, const char* name = "canvas view", WFlags f = 0 ) : QCanvasView( canvas, parent, name, f ), m_elements( elements ) {} protected: void viewportResizeEvent( QResizeEvent *e ); void contentsMousePressEvent( QMouseEvent *e ); void contentsMouseMoveEvent( QMouseEvent *e ); void contentsContextMenuEvent( QContextMenuEvent *e ); private: QCanvasItem *m_movingItem; QPoint m_pos; ElementVector *m_elements; };
我们需要继承QCanvasView,这样我们就能处理:
- 上下文菜单请求。
- 视窗重定义大小。
- 用户拖拽标签到任意位置。
为了支持这些,我们存储一个到正在被移动的画布条目的指针和它的最终位置。我们也存储一个到元素矢量的指针。
上下文菜单请求
(由canvasview.cpp展开。)
void CanvasView::contentsContextMenuEvent( QContextMenuEvent * ) { ((ChartForm*)parent())->optionsMenu->exec( QCursor::pos() ); }
当用户调用一个上下文菜单(比如在绝大多数平台通过右键点击),我们把画布视图的父对象(是一个ChartForm)转化为正确的类型,然后用exec()在光标位置执行选项菜单。
视窗重定义大小
void CanvasView::viewportResizeEvent( QResizeEvent *e ) { canvas()->resize( e->size().width(), e->size().height() ); ((ChartForm*)parent())->drawElements(); }
为了改变大小我们简单地改变花布的大小,画布视图就会呈现在视窗客户端区域的宽高中,然后调用drawElements()函数来重新绘制图表。因为drawElements()画的每一件都和画布的宽高有关,所以图表就会被正确地绘制。
拖拽标签到任意位置
当用户想把标签拖拽到他们点击的位置时,就应该拖拽它并在新的位置释放它。
void CanvasView::contentsMousePressEvent( QMouseEvent *e ) { QCanvasItemList list = canvas()->collisions( e->pos() ); for ( QCanvasItemList::iterator it = list.begin(); it != list.end(); ++it ) if ( (*it)->rtti() == CanvasText::CANVAS_TEXT ) { m_movingItem = *it; m_pos = e->pos(); return; } m_movingItem = 0; }
当用户点击鼠标时,我们创建一个鼠标点击“碰撞”(如果有的话)的画布条目的列表。然后我们迭代这个列表并且如果我们发现一个CanvasText条目,我们就把它设置为移动的条目并且记录下它的位置。否则我们设置为不移动条目。
void CanvasView::contentsMouseMoveEvent( QMouseEvent *e ) { if ( m_movingItem ) { QPoint offset = e->pos() - m_pos; m_movingItem->moveBy( offset.x(), offset.y() ); m_pos = e->pos(); ChartForm *form = (ChartForm*)parent(); form->setChanged( true ); int chartType = form->chartType(); CanvasText *item = (CanvasText*)m_movingItem; int i = item->index(); (*m_elements)[i].setProX( chartType, item->x() / canvas()->width() ); (*m_elements)[i].setProY( chartType, item->y() / canvas()->height() ); canvas()->update(); } }
当用户拖拽鼠标的时候,移动事件就产生了。如果那里是一个可移动条目,我们从鼠标最后的位置和可移动条目原来的位置计算出位移。我们将新的位置记录为最后的位置。因为图表现在改变了,所以我们调用setChanged(),这样当用户试图退出或者读入已存在的图表时或者创建新的图表,就会被提示是否保存。我们也分别地更新当前图表类型的元素的x和y的比例位置为当前x和y与宽和高的比例。我们知道要更新哪个元件因为当我们创建每个画布文本条目的时候,我们传给它一个这个元素所对应的位置索引。我们继承了QCanvasText,这样我们就可以设置和读取这个索引值。最后我们调用update()来重绘画布。
QCanvas没有任何视觉效果。为了看到画布的内容,你必须创建一个QCanvasView来呈现画布。如果条目被show()显示,它们就会出现在画布视图中,然后,只有当QCanvas::update()被调用的时候。默认情况下QCanva的背景是白色,并且会在画布上绘制默认的形状,比如QCanvasRectangle、QCanvasEllipse等等,因为它们被白色填充,所以非常推荐使用一个非白色的画刷颜色! |
« 实现图形用户界面 | 目录 | 文件处理 »
Copyright © 2002 Trolltech | Trademarks | 译者:Cavendish | Qt 3.0.5版
|