前言
设计界面,区分各种形状及画笔颜色、画笔线宽、画笔风格、画笔顶帽、画笔连接点、填充模式、铺展效果、画刷颜色、画刷风格设置等。
一、绘图框架设计
绘制各种图形实例的框架:
首先在 PaintArea 类中完成各种图形显示功能的 Widget,重绘 paintEvent() 函数。然后在主窗口 MainWidget 类中完成各种图形参数的选择。
二、绘图区的实现
PaintArea 类继承自 QWidget 类,在类声明中,首先声明一个枚举型数据 Shape,列举了所有实例可能用到的图形形状;其次声明 setShape() 函数用于设置形状,setPen() 函数用于设置画笔,setBrush() 函数用于设置画刷,setFillRule() 函数用于设置填充模式,以及重绘事件 paintEvent() 函数;最后声明表示各种属性的私有变量。
PaintArea 类的构造函数用于完成初始化工作,设置图形显示区域的背景色及最小显示尺寸,具体代码如下:
PaintArea::PaintArea(QWidget *parent) : QWidget(parent)
{
setPalette(QPalette(Qt::white));
setAutoFillBackground(true);
setMinimumSize(400, 400);
}
PaintArea 类的重画函数代码如下:
void PaintArea::paintEvent(QPaintEvent *)
{
QPainter p(this); //新建一个 QPainter 对象
p.setPen(pen); //设置 QPainter 对象的画笔
p.setBrush(brush); //设置 QPainter 对象的画刷
QRect rect(50, 100, 300, 200); //设定一个方形区域,为画长方形、圆角方形、椭圆形等做准备
static const QPoint points[4] = //创建一个 QPoint 的数组,包含四个点,为画多边形、多边线及点做准备
{
QPoint(150, 100),
QPoint(300, 150),
QPoint(350, 250),
QPoint(100, 300)
};
int startAngle = 30*16; //(c)
int spanAngle = 120*16;
QPainterPath path; //新建一个 QPainterPath 对象为画路径做准备
path.addRect(150, 150, 100, 100);
path.moveTo(100, 100);
path.cubicTo(300, 100, 200, 200, 300, 300);
path.cubicTo(100, 300, 200, 200, 100, 100);
path.setFillRule(fillRule);
switch(shape) //(d)
{
case Line: //直线
p.drawLine(rect.topLeft(), rect.bottomRight()); break;
case Rectangle: //长方形
p.drawRect(rect); break;
case RoundRect: //圆角方形
p.drawRoundRect(rect); break;
case Ellipse: //椭圆形
p.drawEllipse(rect); break;
case Polygon: //多边形
p.drawPolygon(points, 4); break;
case Polyline: //多边线
p.drawPolyline(points, 4); break;
case Points: //点
p.drawPoints(points, 4); break;
case Arc: //弧
p.drawArc(rect, startAngle, spanAngle); break;
case Path: //路径
p.drawPath(path); break;
case Text: //文字
p.drawText(rect, Qt::AlignCenter, QObject::tr("Hello Qt!")); break;
case Pixmap: //图片
p.drawPixmap(150, 150, QPixmap("butterfly.png")); break;
}
}
(c)其中,参数 startAngle 表示起始角,为弧形的起始点与圆心之间连线与水平方向的夹角;参数 spanAngle 表示的是跨度角,为弧形起点、终点分别与圆心连线之间的夹角。
注意:用 QPainter 画弧形所使用的角度值,是以 1/16° 为单位的,在画弧时即 1° 用 16 表示。
(d)switch(shape){…}:使用一个 switch() 语句,对所要画的形状做判断,调用 QPainter 的各个 draw() 函数完成图形的绘制。
(1)利用 QPainter 绘制图形 (Shape) 。
Qt为开发者提供了丰富的绘制基本图形的 draw() 函数,如下所示:
除此之外,QPainter 类还提供了一个 drawPixmap() 函数,可以直接将图片画到刻画控件上。
(2)利用 QPainterPath 绘制简单图形。
利用 QPainterPath 绘制简单图形,QPainterPath 类为 QPainter 类提供了一个存储容器,里面包含了所要绘制的内容的集合及绘制的顺序,如长方形、多边形、曲线等各种任意图形。当需要绘制此预先存储在 QPainterPath 对象中的内容时,只需调用 QPainter 类的 drawPath() 函数即可。
QPainterPath 类提供了许多函数接口,可以很方便地加入一些规则图形。例如,addRect() 函数加入一个方形,addEllipse() 函数加入一个椭圆形,addText() 函数加入一个字符串,addPolygon() 函数加入一个多边形等。同时,QPainterPath 类还提供了 addPath() 函数,用于加入另一个 QPainterPath 对象中保存的内容。
QPainterPath 对象的当前点自动处在上一部分图形内容的结束点上,若下一部分图形的起点不在此结束点,则需调用 moveTo() 函数将当前点移动到下一部分图形的起点。
cubicTo() 函数绘制的是贝塞尔曲线,如下所示。
需要三个参数,分别表示三个点 cubicTo(c1, c2, endPoint);c1,c2 为贝塞尔曲线的控制点。
利用 QPainterPath 类可以实现 QPainter 类的 draw() 函数能够实现的所有图形。
例如,对于 QPainter::drawRect() 函数,除可用上面介绍的 QPainterPath::addRect() 的方式实现外,还可以用如下方式实现:
QPainterPath path;
path.moveTo(0, 0);
path.lineTo(200, 0);
path.lineTo(200, 100);
path.lineTo(0, 100);
path.lineTo(0, 0);
这是一个更通用的方法,其他(如多边形等)图形都能够使用这种方式实现。
三、主窗口的实现
主窗口类 MainWidget 继承自 QWidget 类,包含完成各种图形参数选择的控制区的声明、一系列设置与画图相关参数的槽函数的声明,以及一个绘图区 PaintArea 对象的声明。
MainWidget 类得构造函数中创建了各参数选择控件,代码如下:
MainWidget::MainWidget(QWidget *parent)
: QWidget(parent)
{
paintArea = new PaintArea();
shapeLabel = new QLabel(tr("形状:")); //形状选择下拉列表框
shapeComboBox = new QComboBox();
shapeComboBox->addItem(tr("Line"), PaintArea::Line); //(a)
shapeComboBox->addItem(tr("Rectangle"), PaintArea::Rectangle);
shapeComboBox->addItem(tr("RoundedRect"), PaintArea::RoundRect);
shapeComboBox->addItem(tr("Ellipse"), PaintArea::Ellipse);
shapeComboBox->addItem(tr("Polygon"), PaintArea::Polygon);
shapeComboBox->addItem(tr("Polyline"), PaintArea::Polyline);
shapeComboBox->addItem(tr("Points"), PaintArea::Points);
shapeComboBox->addItem(tr("Arc"), PaintArea::Arc);
shapeComboBox->addItem(tr("Path"), PaintArea::Path);
shapeComboBox->addItem(tr("Text"), PaintArea::Text);
shapeComboBox->addItem(tr("Pixmap"), PaintArea::Text);
connect(shapeComboBox, SIGNAL(activated(int)), this, SLOT(showShape(int)));
penColorLabel = new QLabel(tr("画笔颜色:")); //画笔颜色选择控件
penColorFrame = new QFrame();
penColorFrame->setFrameStyle(QFrame::Panel|QFrame::Sunken);
penColorFrame->setAutoFillBackground(true);
penColorFrame->setPalette(QPalette(Qt::blue));
penColorBtn = new QPushButton(tr("更改"));
connect(penColorBtn, SIGNAL(clicked()), this, SLOT(showPenColor()));
penWidthLabel = new QLabel(tr("画笔线宽:")); //画笔线宽选择控件
penWidthSpinBox = new QSpinBox();
penWidthSpinBox->setRange(0, 20);
connect(penWidthSpinBox, SIGNAL(valueChanged(int)), this, SLOT(showPenWidth(int)));
penStyleLabel = new QLabel(tr("画笔风格:")); //画笔风格选择下拉列表框
penStyleComboBox = new QComboBox();
penStyleComboBox->addItem(tr("SolidLine"), static_cast<int>(Qt::SolidLine)); //(b)
penStyleComboBox->addItem(tr("DashLine"), static_cast<int>(Qt::DashLine));
penStyleComboBox->addItem(tr("DotLine"), static_cast<int>(Qt::DotLine));
penStyleComboBox->addItem(tr("DashDotLine"), static_cast<int>(Qt::DashDotLine));
penStyleComboBox->addItem(tr("DashDotDotLine"), static_cast<int>(Qt::DashDotDotLine));
penStyleComboBox->addItem(tr("CustomDashLine"), static_cast<int>(Qt::CustomDashLine));
connect(penStyleComboBox, SIGNAL(activated(int)), this, SLOT(showPenStyle(int)));
penCapLabel = new QLabel(tr("画笔顶帽:")); //画笔顶帽风格选择下拉列表框
penCapComboBox = new QComboBox();
penCapComboBox->addItem(tr("SquareCap"), Qt::SquareCap); //(c)
penCapComboBox->addItem(tr("FlatCap"), Qt::FlatCap);
penCapComboBox->addItem(tr("RoundCap"), Qt::RoundCap);
connect(penCapComboBox, SIGNAL(activated(int)), this, SLOT(showPenCap(int)));
penJoinLabel = new QLabel(tr("画笔连接点:")); //画笔连接点风格选择下拉列表框
penJoinComboBox = new QComboBox();
penJoinComboBox->addItem(tr("BevelJoin"), Qt::BevelJoin); //(d)
penJoinComboBox->addItem(tr("MiterJoin"), Qt::MiterJoin);
penJoinComboBox->addItem(tr("RoundJoin"), Qt::RoundJoin);
connect(penJoinComboBox, SIGNAL(activated(int)), this, SLOT(showPenJoin(int)));
fillRuleLabel = new QLabel(tr("填充模式:")); //填充模式选择下拉列表框
fillRuleComboBox = new QComboBox();
fillRuleComboBox->addItem(tr("Odd Even"), Qt::OddEvenFill); //(e)
fillRuleComboBox->addItem(tr("Winding"), Qt::WindingFill);
connect(fillRuleComboBox, SIGNAL(activated(int)), this, SLOT(showFillRule()));
spreadLabel = new QLabel(tr("铺展效果:")); //铺展效果选择下拉列表框
spreadComboBox = new QComboBox();
spreadComboBox->addItem(tr("PadSpread"), QGradient::PadSpread); //(f)
spreadComboBox->addItem(tr("RepeatSpread"), QGradient::RepeatSpread);
spreadComboBox->addItem(tr("ReflectSpread"), QGradient::ReflectSpread);
connect(spreadComboBox, SIGNAL(activated(int)), this, SLOT(showSpreadStyle()));
brushColorLabel = new QLabel(tr("画刷颜色:")); //画刷颜色选择控件
brushColorFrame = new QFrame();
brushColorFrame->setFrameStyle(QFrame::Panel|QFrame::Sunken);
brushColorFrame->setAutoFillBackground(true);
brushColorFrame->setPalette(QPalette(Qt::green));
brushColorBtn = new QPushButton(tr("更改"));
connect(brushColorBtn, SIGNAL(clicked()), this, SLOT(showBrushColor()));
brushStyleLabel = new QLabel(tr("画刷风格:")); //画刷风格选择下拉列表框
brushStyleComboBox = new QComboBox();
brushStyleComboBox->addItem(tr("SolidPattern"), static_cast<int>(Qt::SolidPattern)); //(g)
brushStyleComboBox->addItem(tr("Densel1Pattern"), static_cast<int>(Qt::Dense1Pattern));
brushStyleComboBox->addItem(tr("Densel2Pattern"), static_cast<int>(Qt::Dense2Pattern));
brushStyleComboBox->addItem(tr("Densel3Pattern"), static_cast<int>(Qt::Dense3Pattern));
brushStyleComboBox->addItem(tr("Densel4Pattern"), static_cast<int>(Qt::Dense4Pattern));
brushStyleComboBox->addItem(tr("Densel5Pattern"), static_cast<int>(Qt::Dense5Pattern));
brushStyleComboBox->addItem(tr("Densel6Pattern"), static_cast<int>(Qt::Dense6Pattern));
brushStyleComboBox->addItem(tr("Densel7Pattern"), static_cast<int>(Qt::Dense7Pattern));
brushStyleComboBox->addItem(tr("HorPattern"), static_cast<int>(Qt::HorPattern));
brushStyleComboBox->addItem(tr("VerPattern"), static_cast<int>(Qt::VerPattern));
brushStyleComboBox->addItem(tr("CrossPattern"), static_cast<int>(Qt::CrossPattern));
brushStyleComboBox->addItem(tr("BDiagPattern"), static_cast<int>(Qt::BDiagPattern));
brushStyleComboBox->addItem(tr("FDiagPattern"), static_cast<int>(Qt::FDiagPattern));
brushStyleComboBox->addItem(tr("DiagCrossPattern"), static_cast<int>(Qt::DiagCrossPattern));
brushStyleComboBox->addItem(tr("LinearGradientPattern"), static_cast<int>(Qt::LinearGradientPattern));
brushStyleComboBox->addItem(tr("ConicalGradientPattern"), static_cast<int>(Qt::ConicalGradientPattern));
brushStyleComboBox->addItem(tr("RadialGradientPattern"), static_cast<int>(Qt::RadialGradientPattern));
brushStyleComboBox->addItem(tr("TexturePattern"), static_cast<int>(Qt::TexturePattern));
connect(brushStyleComboBox, SIGNAL(activated(int)), this, SLOT(showBrush(int)));
rightLayout = new QGridLayout(); //控制面板布局
rightLayout->addWidget(shapeLabel, 0, 0);
rightLayout->addWidget(shapeComboBox, 0, 1);
rightLayout->addWidget(penColorLabel, 1, 0);
rightLayout->addWidget(penColorFrame, 1, 1);
rightLayout->addWidget(penColorBtn, 1, 2);
rightLayout->addWidget(penWidthLabel, 2, 0);
rightLayout->addWidget(penWidthSpinBox, 2, 1);
rightLayout->addWidget(penStyleLabel, 3, 0);
rightLayout->addWidget(penStyleComboBox, 3, 1);
rightLayout->addWidget(penCapLabel, 4, 0);
rightLayout->addWidget(penCapComboBox, 4, 1);
rightLayout->addWidget(penJoinLabel, 5, 0);
rightLayout->addWidget(penJoinComboBox, 5, 1);
rightLayout->addWidget(fillRuleLabel, 6, 0);
rightLayout->addWidget(fillRuleComboBox, 6, 1);
rightLayout->addWidget(spreadLabel, 7, 0);
rightLayout->addWidget(spreadComboBox, 7, 1);
rightLayout->addWidget(brushColorLabel, 8, 0);
rightLayout->addWidget(brushColorFrame, 8, 1);
rightLayout->addWidget(brushColorBtn, 8, 2);
rightLayout->addWidget(brushStyleLabel, 9, 0);
rightLayout->addWidget(brushStyleComboBox, 9, 1);
QHBoxLayout *mainLayout = new QHBoxLayout(this); //整体布局
mainLayout->addWidget(paintArea);
mainLayout->addLayout(rightLayout);
mainLayout->setStretchFactor(paintArea, 1);
mainLayout->setStretchFactor(rightLayout, 0);
showShape(shapeComboBox->currentIndex()); //显示默认的图形
}
(a)QComboBox 的 additem() 函数可以仅插入文本,也可以同时插入与文本相对应的具体数据,通常为枚举类型数据,便于后面操作时确定选择的是哪个数据。
(b)选用不同的参数,对应画笔的不同风格。
(c)选用不同的参数,对应画笔顶帽的不同风格。其中,Qt::SquareCap 表示在线条的顶点处是方形的,且线条绘制的区域包括了端点,并且再往外延伸半个线宽的长度;Qt::FlatCap 表示在线条的顶点处是方形的,但线条绘制区域不包括端点在内;Qt::RoundCap 表示在线条的顶点处是圆形的,且线条绘制区域包含了端点。
(d)选用不同的参数,对应画笔连接点的不同风格。其中,Qt::BevelJoin 风格连接点是指两条线的中心线顶点相汇,相连处依然保留线条各自的方形顶端;Qt::MiterJoin 风格连接点是指两条线的中心线顶点相汇,相连处线条延长到线的外侧汇集至点,形成一个尖顶的连接;Qt::RoundJoin 风格连接点是指两条线的中心线顶点相汇,相连处以圆弧形连接。
(e)Qt 为 QPainterPath 类提供了两种填充规则,分别是 Qt::OddEvenFill 和 Qt::WindingFill。这两种填充规则在判定图形中某一点是处于内部还是外部时,判断依据不同。
其中,Qt::OddEvenFill 填充规则判断的依据是从图形中某一点画一条水平线到图形外。若这条水平线与图形边线的交点数目为奇数,则说明此点位于图形的内部;若交点数目为偶数,则此点位于图形的外部。
而 Qt::WindingFill 填充规则的判断依据则是从图形中某一点画一条水平线到图形外,每个交点外边线的方向可能向上,也可能向下,将这些交点数累加,方向相反的相互抵消,若最后结果不为 0 则说明此点在图形内,若最后结果为 0 则说明在图形外。
其中,边线的方向是由 QPainterPath 创建时根据描述的顺序决定的。如果采用 addRect() 或 addPolygon() 等函数加入的图形,默认是按顺时针方向。
(f)铺展效果有三种,分别为 QGradient::PadSpread、QGradient::RepeatSpread 和 QGradient::ReflectSpread。其中,PadSpread 是默认的铺展效果,也是最常见的铺展效果,没有被渐变覆盖的区域填充单一的起始颜色或终止颜色;RepeatSpread 效果与 ReflectSpread 效果只对线性渐变和圆形渐变起作用。
使用 QGradient 的 setColorAt() 函数设置起止的颜色,其中,第 1 个参数表示所设颜色点的位置,取值范围为 0.0~1.0,0.0 表示起点,1.0 表示终点;第 2 个参数表示该点的颜色值。除可设置起点和终点的颜色外,如有需要还可设置中间任意位置的颜色,例如:setColorAt(0.3, Qt::white),设置起、终点之间 1/3 位置的颜色为白色。
(g)选用不同的参数,对应画刷的不同风格。
showShape() 槽函数,根据当前下拉列表框中选择的选项,调用 PaintArea 类的 setShape() 函数设置 PaintArea 对象的形状参数,代码如下:
void MainWidget::showShape(int value)
{
PaintArea::Shape shape = PaintArea::Shape(shapeComboBox->itemData(value, Qt::UserRole).toInt());
paintArea->setShape(shape);
}
其中,QComboBox 类的 itemData 方法返回当前显示的下拉列表框数据,是一个 QVariant 对象,此对象与控件初始化时插入的枚举型数据相关,调用 QVariant 类的 toInt() 函数获得此数据在枚举型数据集合中的序号。
showBrush() 槽函数的具体实现代码如下:
void MainWidget::showBrush(int value)
{
//获得画刷的颜色
QColor color = brushColorFrame->palette().color(QPalette::Window);
Qt::BrushStyle style = Qt::BrushStyle(brushStyleComboBox->itemData(value, Qt::UserRole).toInt()); //(a)
if(style == Qt::LinearGradientPattern) //(b)
{
QLinearGradient linearGradient(0, 0, 400, 400);
linearGradient.setColorAt(0.0, Qt::white);
linearGradient.setColorAt(0.2, color);
linearGradient.setColorAt(1.0, Qt::black);
linearGradient.setSpread(spread);
paintArea->setBrush(linearGradient);
}
else if(style == Qt::RadialGradientPattern) //(c)
{
QRadialGradient radialGradient(200, 200, 150, 150, 100);
radialGradient.setColorAt(0.0, Qt::white);
radialGradient.setColorAt(0.2, color);
radialGradient.setColorAt(1.0, Qt::black);
radialGradient.setSpread(spread);
paintArea->setBrush(radialGradient);
}
else if(style == Qt::ConicalGradientPattern) //(d)
{
QConicalGradient conicalGradient(200, 200, 30);
conicalGradient.setColorAt(0.0, Qt::white);
conicalGradient.setColorAt(0.2, color);
conicalGradient.setColorAt(1.0, Qt::black);
paintArea->setBrush(conicalGradient);
}
else if(style == Qt::TexturePattern)
{
paintArea->setBrush(QBrush(QPixmap("image.png")));
}
else
{
paintArea->setBrush(QBrush(color, style));
}
}
(a)获得所选的画刷风格,若选择的是渐变或者纹理图案,则需要进行一定的处理。
(b)主窗口的 style 变量值为 Qt::LinearGradientPattern 时,表明选择的是线形渐变。
QLinearGradient linearGradient(startPoint, endPoint) 创建线形渐变类对象需要两个参数,分别表示起止点位置。
(c)主窗口的 style 变量值为 Qt::RadialGradientPattern 时,表明选择的是圆形渐变。
QRadialGradiend radialGradient(startPoint, r, endPoint) 创建圆形渐变类对象需要三个参数,分别表示圆心位置、半径值和焦点位置。表示以 startPoint 作为圆心和焦点的位置,以 startPoint 和 endPoint 之间的距离 r 为半径,当然圆心和焦点的位置也可以不重合。
(d)主窗口的 style 变量值为 Qt::ConicalGradientPattern 时,表明选择的是锥形渐变。
QConicalGradient conicalGradient(startPoint, -(180*angle)/PI) 创建锥形渐变类对象需要两个参数,分别是锥形的顶点位置和渐变分界线与水平方向的夹角。锥形渐变不需要设置铺展效果,它的铺展效果只能是 QGradient::PadSpread。
注意:锥形渐变的方向默认是逆时针方向。
Qt 画图的坐标系默认以左上角为原点,X 轴向右,Y 轴向下。此坐标系可受 QPainter 类控制,对其进行变形,QPainter 类提供了相应的变形函数,包括旋转、缩放、平移和切变。调用这些函数时,显示设备的坐标系会发生相应变形,但绘制内容相对坐标系的位置并不会发生改变,因此看起来像是对绘制内容进行了变形,而实质上是坐标系的变形。若还需要更加复杂的变形,则可以采用 QMatrix 类实现。