简单的饼图控件






4.85/5 (27投票s)
本文旨在介绍一个用于 MFC 应用程序的简单饼图控件。

引言
本文旨在介绍一个用于 MFC 应用程序的简单饼图控件。网上有很多图表控件和库,但有些在图形方面功能不足或过于复杂,对于小型项目来说会有些负担。这个饼图是使用 GDI+ 图形库为 MFC 平台编写的,它使用的类(单个类)和提供的基本功能都很简单。它有三种饼图样式。它们是:
- 甜甜圈样式。
- 二维样式。
- 三维样式。
它包含了为每个饼图元素、背景和文本区域设置颜色等功能。还提供了设置文本字体、设置旋转以及为 3D 样式设置倾斜度的功能。
背景
该实现使用了 GDI+ 的基本绘图功能。但有三个属性提供了控件的视觉效果。它们是:
- 颜色的渐变计算。
- 无闪烁绘制。
- 3D 饼图计算。
计算不同颜色的渐变
它使用简单的线性计算来查找渐变颜色。计算给定颜色的渐变如下:
- 单独获取给定颜色的 RGB 值。
- 要计算深色渐变,将 RGB 值除以一个公共因子,然后乘以用户设置的因子,该因子等于或小于除法因子。
例如,对于 R 值,R 渐变 = (R / 除法因子) * 因子(线性方程)
- 然后,将此值从原始 R 中减去,计算出 R 新值,从而为相应的颜色创建深色渐变值。
除法因子使用 255。选择 255 的原因是,对于任何 RGB 值,其最大值都可以是 255。
要计算浅色渐变,将每个 RGB 值减去 255,然后除以 255 来找到渐变。
将此值乘以一个因子并加到原始 RGB 值上。
Color CPieChartWnd::CalculateGradientLight(Color crBase, float fGradVal)
{
BYTE r = crBase.GetR();
BYTE g = crBase.GetG();
BYTE b = crBase.GetB();
float fact = 255.0f;
float rGrad = (255 - r) / fact;
float gGrad = (255 - g) / fact;
float bGrad = (255 - b) / fact;
r = BYTE(min(r + rGrad * fGradVal, 255));
b = BYTE(min(b + bGrad * fGradVal, 255));
g = BYTE(min(g + gGrad * fGradVal, 255));
return Color(r, g, b);
}
Color CPieChartWnd::CalculateGradientDark(Color crBase, float fGradVal)
{
BYTE r = crBase.GetR();
BYTE g = crBase.GetG();
BYTE b = crBase.GetB();
float fact = 255.0f;
float rGrad = r / fact;
float gGrad = g / fact;
float bGrad = b / fact;
r = BYTE(max(r - rGrad * fGradVal, 0));
b = BYTE(max(b - bGrad * fGradVal, 0));
g = BYTE(max(g - gGrad * fGradVal, 0));
return Color(r, g, b);
}


无闪烁绘制
GDI+ 中的双缓冲是通过 `Bitmap` 和 `CachedBitmap` 对象实现的。我的使用方法是使用 `Graphics::FromImage` 创建一个 `Graphics` 对象。传入的 `Bitmap` 的大小与 Cwnd 区域相同。然后,所有绘图操作都在 `Graphics` 对象上进行。最后,释放此 `Graphics` 对象,并将 `Bitmap` 用于创建 `CachedBitmap`,它将从设备上下文中创建的图形成员构建。一种技术是保存 `Bitmap` 对象,并在调整大小时或绘图更改时将其设置为脏。其他时候,保存的位图将直接在 `CachedBitmap` 上使用。但在本文中,我只在 `onPaint` 调用时创建和销毁 `Bitmap`。
Bitmap* mBtmap = new Bitmap(rect.Width(), rect.Height());
graphics = Graphics::FromImage(mBtmap);
//
//rest of the drawings done on graphics object
//
delete graphics;
graphics = NULL;
Graphics gr(pDc->m_hDC);
gr.SetSmoothingMode(SmoothingModeHighQuality);
CachedBitmap* btmp = new CachedBitmap(mBtmap, &gr);
if (mBtmap){
delete mBtmap;
mBtmap = NULL;
}
gr.DrawCachedBitmap(btmp, rect.left, rect.top);
if (btmp){
delete btmp;
btmp = NULL;
}
3D 对象计算
对于 3D 饼图,它需要根据倾斜角度对绘图参数进行一些变换。因此,当设置倾斜角度时,圆形饼图会变成椭圆形,导致元素饼图区域发生视觉变化。这些区域完全根据每个元素的饼图角度变化进行变换。该角度计算如下:

因此,计算是:
新点 y' = y + x * sin (α)。使用点的新的位置和椭圆的中心点,计算新的角度 α。
void CPieChartWnd::UpdatePiechartPoints(void)
{
//The calculated pie element points are relocated according to the
//incline angle and the resulting formations of angles were calculated and set.
CRect rectBnd;
GetBoundRect(rectBnd);
//Calculate (set) the original locations for the points prior to the
//circle it bounds.
CalcuatePieElemetPoints();
float flStart = fl_startAngle;
float flStartIncline = 0;
PointF ptStart;
CRect rectBtm, rectTop;
Get3DBounds(rectTop, rectBtm);
long rectClip = long(fl_InclineAngle * rectBnd.Height() / 180);
rectTop.top += long(rectClip * f_depth / 2);
rectTop.bottom += long(rectClip * f_depth / 2);
REAL xPoint = REAL(rectBnd.CenterPoint().x +
(rectBnd.Width() / 2) * cos(PI * (flStart)/ 180));
REAL yPoint = REAL(rectBnd.CenterPoint().y +
(rectBnd.Height() / 2) * sin(PI * (flStart)/ 180));
ptStart.X = xPoint;
//Relocate the start angle y cordinate according to the inclination
ptStart.Y = yPoint - REAL(rectClip * sin((flStart) * PI / 180));
flStartIncline = CacluateInclineAngle(ptStart, rectTop);
fl_startAngleIncline = flStartIncline;
map<int, pie_chart_element*>::reverse_iterator iter = map_pChart.rbegin();
//Relocate the y coordinates according to the incline angle and recalculate
//the angles for elements
for (; iter != map_pChart.rend(); iter++){
pie_chart_element* ele = iter->second;
ele->pie_3d_props.pt_InPie.Y -= REAL(
rectClip * sin((flStart + ele->f_angle) * PI / 180));
float inClineAngle = CacluateInclineAngle(ele->pie_3d_props.pt_InPie,
rectTop);
if(inClineAngle == flStartIncline && ele->f_angle == 360)
// Two points lies in the same location and the angle is 360
inClineAngle = 360;
else if (inClineAngle >= flStartIncline)
inClineAngle -= flStartIncline;
else
inClineAngle += (360 - flStartIncline);
ele->pie_3d_props.f_InclineAngle = inClineAngle;
flStartIncline += inClineAngle;
flStart += ele->f_angle;
}
}
使用控件
通过在客户端框架上创建一个 `CPieChartwnd` 成员,可以轻松使用此控件。
BOOL CPieChartWnd::Create(LPCTSTR lpCaption, const RECT& rect, CWnd* pParentWnd,
UINT nID)
提供了各种函数来设置颜色、饼图样式、字体和颜色渐变等视觉属性。通过在计时器中设置起始角度可以创建旋转效果,通过设置倾斜角度可以为 3D 样式图表创建倾斜效果。此外,它还具有一些对元素进行排序以及恢复到添加顺序的功能。
饼图元素结构
struct pie_3d_properties{
float f_InclineAngle; //The transformed angle for 3-D pie chart
PointF pt_InPie; //The location point for a single element on the face
//of pie
GraphicsPath path; //The visible path object from side view
};
struct pie_chart_element{
double d_value;
float f_percentage;
float f_angle;
float f_ColorGradL;
float f_ColorGradD;
Color cr_GradientL;
Color cr_GradientD;
Color cr_Base;
CString s_label;
CString s_element;
pie_3d_properties pie_3d_props;
int i_ID;
BOOL b_select;
};
饼图元素有两个唯一的键。一个是字符串键,用户可以在添加元素时定义。另一个是整数标识符。这是由控件本身添加的。字符串键必须是唯一的且不能为空字符串。
添加元素
定义了 `InsertItem` 方法来向图表中添加元素。如果插入成功,它将返回一个指向该元素的指针,该元素是 `pie_chart_element` 的定义数据类型。返回的指针类型是 `PIECHARTITEM`。
可以使用两个键或使用 `PIECHARTITEM` 指针删除元素。
PIECHARTITEM InsertItem(CString sElement, CString sLabel, double dValue, Color crColor);
//Remove item functions
BOOL RemoveItem(CString sElement);
BOOL RemoveItem(int iElementID);
BOOL RemoveItem(PIECHARTITEM pItem);
关注点
此控件仅提供饼图控件应有的基本功能,但对于小型 MFC 应用程序会很有用。此外,这也可以作为 GDI+ 图形学习材料。