65.9K
CodeProject 正在变化。 阅读更多。
Home

用 7 个简单步骤构建图形控件。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (25投票s)

2003年3月13日

7分钟阅读

viewsIcon

147416

downloadIcon

2839

本文将通过 7 个简单步骤展示如何创建一个简单的用户控件,即图形控件。

Sample Image - maximum width is 600 pixels

引言

C# 提供了丰富的功能来构建我们自己的可重用控件。众所周知,第三方控件通常价格昂贵,并且不具备我们所需的确切功能,因此我们必须编写额外的代码才能实现我们可能会一遍又一遍使用的某些特殊功能。此外,由于我们没有第三方组件的代码,因此无法对其代码进行修改来改变其行为。

因此,这里有一个您可能需要在应用程序中添加的图形控件,您可以轻松地将其添加到窗体中,并在几分钟内即可在窗体上显示图形。这就像将一个 ButtonTextBox 添加到窗体一样简单。

图形控件提供以下功能

  1. 图形样式:折线图或条形图
  2. 图形颜色:您可以设置显示文本、轴和实际图形的颜色。
  3. 显示点:您可以选择显示坐标的文本表示以及图形。
  4. 刻度:X 轴和 Y 轴具有不同的刻度,并可以选择其标题。

使用代码

如您在上图所示,我有一个带有图形控件的演示窗体,并且我向该窗体添加了其他控件,以便在运行时添加/修改图形控件的功能。

让我们逐步了解 GraphControl 的构建过程,以了解用户控件是如何构建的。

让我们开始吧……

GraphControl 的定义如下

namespace UserControls 
{
    public class GraphControl : System.Windows.Forms.UserControl
    {
     .......

我们创建一个命名空间 UserControls,并创建一个从 UserControl 派生的类 GraphControl,这将其定义为用户控件。

结构和枚举

在深入探讨 GraphControl 的细节之前,让我先定义在 UserControls 命名空间中定义的其他结构和枚举。

  1. GraphStruct
    public struct GraphStruct
    {
        public int PointsOnXAxis,PointsOnYAxis;
        public float StartValueXAxis,StartValueYAxis;
        public int StepXAxis,StepYAxis;
        ...
        ..
    }

    上述结构用于初始化图形,并将决定如何绘制轴以及如何布局坐标。

    PointsOnXAxisPointsOnYAxis

    这些变量将定义要在轴上布置的点数。例如,如果 x 轴代表工程课程的年数,y 轴代表学生每年的百分比/成绩,则 PointsOnXAxis 为 4,PointsOnYAxis 为 10。

    StartValueXAxisStartValueYAxis

    这些变量定义 X 轴和 Y 轴上的第一个值是什么。以上面的例子为例,如果课程始于 1994 年,则 x 轴将从 1994 年开始;如果成绩范围从 10% 到 100%,则 y 轴将从 10 开始。

    StepXAxisStepYAxis

    这些变量将定义每个轴上坐标的增量。同样,以上面的例子为例,x 轴的步长将是 1,因为我们逐年递增;而 y 轴的增量将是 10,因为我们以 10 为步长递增。

    此示例的图形如下所示

    Grades

  2. FloatPoint
    public struct FloatPoint
    {
        public float X,Y,Value;
        ..
        .
    }

    此结构用于表示图形上的一个点。类 Point 不足以存储我们的点的原因是我们的坐标是 float 类型,而 Point 只接受整数。此外,我们还有一个额外的成员 Value,用于表示实际的显示值。

    例如,我们想在图形上显示一个点 (1994,75)。

    1. 此点在我们的图形上被分成两个特定的点:x 轴上的 (1994,0) 和 y 轴上的 (0,75)。
    2. 现在,1994 或 57 仅仅是显示值。它们的实际坐标由我们类中的一个名为 FinLocationOnGraph 的函数计算,该函数的作用类似于 Windows 的 ScreenToClient 函数。因此,这些点会变成 float 点,其值如下:

      X = 134 ,Y = 561, Value = 1994 => 表示 (1994,0)

      X = 97, Y = 351 , Value = 57 => 表示 (0,57)

  3. GraphType
    public enum GraphType
    {
        Line,
        Bar
    }

    上述枚举用于决定 graph.BarGraphLineGraph 的样式。

函数

现在让我们看看 GraphControl 是如何实现的……

  • DrawAxes 函数
    //draws the X and Y axes and also set the 
    //origin and x and y axes points
    //This method should be called before any other drawing method.
    void DrawAxes(Graphics g)
    {
        //get the bounds of our region.We will draw within the region
        RectangleF rect = Region.GetBounds(g);
        float xOrigin =  rect.Left  + 20;
        float yOrigin =  rect.Bottom - 70;
                
        origin = new FloatPoint(xOrigin,yOrigin);
        xAxis = new FloatPoint(rect.Right - 20,origin.Y);
        yAxis = new FloatPoint(origin.X ,rect.Top);
                
        Pen axisPen = new Pen(axesColor);
                    
        g.DrawLine(axisPen,origin.X,origin.Y,xAxis.X,xAxis.Y);
        g.DrawLine(axisPen,origin.X,origin.Y,yAxis.X,yAxis.Y);
                            
        axisPen.Dispose();
    }

    此方法绘制轴。它执行以下操作:

    1. 获取控件绘制区域的边界。
    2. 定义原点,该原点将位于控件的 (left, bottom) 点。
    3. 定义 x 轴,该轴将从 (left, bottom) 运行到 (right, bottom)。
    4. 定义 y 轴,该轴将从 (left, bottom) 运行到 (left, top)。
  • DrawPoints 函数
    //draws the co-ordinates on x and y axes.
    void DrawPoints(Graphics g)
    {
        float xDiff = xAxis.X - origin.X;
        float yDiff = origin.Y - yAxis.Y;
        float xStep = xDiff/GRAPHSTRUCT.PointsOnXAxis;
        float yStep = yDiff/GRAPHSTRUCT.PointsOnYAxis;
                
        FloatPoint fpt = new FloatPoint(origin.X,origin.Y);
        fpt.Value = 0;
        graphPointsX.Add(fpt);
        graphPointsY.Add(fpt);
                
        Pen   p = new Pen(textColor);
        Brush b = new SolidBrush(textColor);
        Font  f = new Font(Font.FontFamily,Font.Size);
        for(int i = 1; i<= GRAPHSTRUCT.PointsOnXAxis; i++)
        {
            float xAxisX = origin.X + (i * xStep);
            float xAxisY = origin.Y;
                    
            g.DrawLine(p,xAxisX,xAxisY - 2,xAxisX,xAxisY + 2);
            float val = GRAPHSTRUCT.StartValueXAxis + 
                            ((i-1) * GRAPHSTRUCT.StepXAxis) ;    
            g.DrawString(val.ToString(),f,b,xAxisX-5,xAxisY + 3);
                    
            fpt.X = xAxisX;
            fpt.Y = 0;
            fpt.Value = val;
            graphPointsX.Add(fpt);
        }
        for(int j = 1; j<= GRAPHSTRUCT.PointsOnYAxis; j++)
        {
            float yAxisX = origin.X;
            float yAxisY = origin.Y - (j * yStep);
                    
            g.DrawLine(p,yAxisX -2 ,yAxisY,yAxisX + 2,yAxisY);
            float val = GRAPHSTRUCT.StartValueYAxis + 
                          ((j-1) * GRAPHSTRUCT.StepYAxis) ;    
            g.DrawString(val.ToString(),f,b,yAxisX-15,yAxisY);
                    
            fpt.X = 0;
            fpt.Y = yAxisY;
            fpt.Value = val;
            graphPointsY.Add(fpt);
        }
        f.Dispose();
        b.Dispose();
        p.Dispose();
    }

    此函数绘制 X 和 Y 轴上的坐标。

    对于 X 轴

    1. 计算 origin 和 X 轴最后一个坐标之间的差值 xDiff
    2. 计算 xStep,即点/坐标比率。这将是 X 轴上每个绘制的坐标之间的距离。
    3. 将在 X 轴的每个坐标处绘制一个小线段和坐标值。
    4. 所有这些点都将保存在一个列表 graphPointsX 中。

    对 Y 轴将遵循类似的程序,点将存储在 graphPointsY 中。

  • FindLocationOnGraph 函数。
    //Given a display point, 
    //this function will point the actual co-ordinates
    //for this point on our graph. 
    //It acts like the ScreenToClient function in Windows
    FloatPoint FindLocationOnGraph(Point pt)
    {
         float diffX,diffY,diffValue,finalXValue,finalYValue;
         diffX = diffY = -1;
         finalXValue = finalYValue = 0;
         for(int i=0;i < graphPointsX.Count;i++)
         {
             //store the current point
             FloatPoint current = (FloatPoint)graphPointsX[i];
             //if points X is lesser that current points Value
             if((float)pt.X < current.Value)
             {
                 FloatPoint previous = (FloatPoint)graphPointsX[i-1];
                 //store diff between current
                 // X and previous X coordinate
                 diffX = current.X - previous.X;
                 //store difference between values 
                 //of current and prev points
                 diffValue = current.Value - previous.Value;
                 float unitsPerCoordinate = diffValue/diffX;
                 finalXValue = 
                   ((pt.X - previous.Value)/unitsPerCoordinate) 
                   + previous.X;
                 break;
             }
             else if((float)pt.X == current.Value)
             {
                 finalXValue = current.X;
             }
         }
         for(int j=0;j < graphPointsY.Count;j++)
         {
             FloatPoint current = (FloatPoint)graphPointsY[j];
             if((float)pt.Y < current.Value)
             {
                 FloatPoint previous = (FloatPoint)graphPointsY[j-1];
                 diffY     = current.Y     - previous.Y;
                 diffValue = current.Value - previous.Value;
                 float unitsPerCoordinate = diffValue/diffY;
                 finalYValue = 
                      ((pt.Y - previous.Value)/unitsPerCoordinate) 
                      + previous.Y;
                 break;
             }
             else if((float)pt.Y == current.Value)
             {
                 finalYValue = current.Y;
             }
         }
         FloatPoint fpNew = new FloatPoint(finalXValue,finalYValue);
         return fpNew;
    }
    

    此函数将通过将其转换为图形控件可以理解的浮点坐标来返回点 (1996,66) 的实际坐标。

    代码可能看起来难以理解,但实际上很简单。它的工作原理如下:

    让我们假设以下情况:

    1. 原点 (0,0) 位于图形的 (45,45) 位置。
    2. 点 (0,60) 位于 Y 轴上的 (45,100)。
    3. 点 (0,70) 位于 Y 轴上的 (45,150)。

    因此,要找到 Y 轴上 (1999,66) 的确切位置,我们执行以下操作:

    1. 找到 Y 轴上大于 66 的一个点,即 70。
    2. 找到小于 66 的前一个点,即 60。
    3. 计算这些点的坐标之间的差值 diffY。即 150 - 100 = 50。
    4. 计算两个值之间的差值 diffValue,即 70 - 60 = 10。
    5. 计算 unitsPerCoordinate=diffValue/diffY,即 10/50 = 0.2。
    6. 计算小于我们 Points 值的点与我们 Points 值之间的差值。即 66-60 = 6。
    7. 这意味着我们的点 66 比 60 超前 (6 / 0.2) 个 graphPoints
    8. 因此,66 的 finalYValue 将等于 60 的位置 + (6 / 0.2),即 100 + 30 = 130。
    9. 点 (0,66) 将在 Y 轴上定位为 (45,130)。
    10. 类似地,我们找到 X 轴上的位置 (1996, 0)。
    11. 然后我们创建一个新的 FloatPoint,其中包含 finalXValuefinalYValue
  • DrawLineGraph 函数
    //draws the actual graph, Line style.
    void DrawLineGraph(Graphics g)
    {
        Pen p = new Pen(graphColor);
        Point start = (Point)Points[0];
        FloatPoint prev,current;
        prev = FindLocationOnGraph(start);
        for(int i=1 ; i < Points.Count ;i++)
        {
            Point pt = (Point)Points[i];
            current = FindLocationOnGraph(pt);    
            g.DrawLine(p,prev.X,prev.Y,current.X,current.Y);
            if(bShowPoints)
            {
                Brush b = new SolidBrush(textColor);
                Font f = new Font(Font.FontFamily,Font.Size);    
                string title = "(" + pt.X + "," + pt.Y + ")";
                if(prev.Y > current.Y)
                    g.DrawString(title,f,b,current.X - 25, 
                       current.Y - 15);
                else
                    g.DrawString(title,f,b,
                       current.X - 25, current.Y + 5);
                if(i ==1)
                {
                    title = "(" + start.X + "," + start.Y + ")";
                    g.DrawString(title,f,b,prev.X - 10, prev.Y - 15);
                }
                f.Dispose();
                b.Dispose();
            }
            prev = current;
        }
        p.Dispose();
    }

    此函数绘制折线图。它非常简单。它的作用是:

    1. Points 列表中获取第一个 Point
    2. 通过将其传递给 FindLocationOnGraph 来查找其位置。
    3. 将其存储在 FloatPoint prev 中。
    4. 获取下一个点,并为其重复步骤 2,并将其存储在 FloatPoint current 中。
    5. prevcurrent 绘制一条线。
    6. 如果 ShowPointsOnGraph 属性为 true,则绘制坐标。
    7. 如果正在绘制的线向下倾斜,则将坐标绘制在线上方,否则绘制在下方。
    8. current 点存储到 prev 中。
    9. 重复步骤 4 到 7,直到绘制完列表中的每个点。
  • OnPaint 函数
    //overridden to paint our graph
    protected override void OnPaint(PaintEventArgs pe)
    {
        Rectangle bound = new Rectangle(new Point(0,0),Size);
        graphRegion = new Region(bound);
        Region = graphRegion;
        DrawAxes(pe.Graphics);
        DrawPoints(pe.Graphics);
        DrawTitles(pe.Graphics);
        if(graphType == GraphType.Bar)
           DrawBarGraph(pe.Graphics);
        else
           DrawLineGraph(pe.Graphics);
        //drawAxes is called twice becuase 
        //the axes get overwwiten sometimes by other lines
        DrawAxes(pe.Graphics);
    }

    如果您正在构建自己的用户控件,这是**最重要的函数**。重写此函数以绘制您的控件。这是真正发挥作用的地方。

    • 步骤 1. 我们首先通过控件的大小来设置绘制区域的边界。
    • 步骤 2. 然后我们使用边界定义 graphRegion,图形将在其中绘制。
    • 步骤 3. 然后我们绘制轴。
    • 步骤 4. 然后在轴上绘制点。
    • 步骤 5. 然后绘制轴的标题。
    • 步骤 6. 根据 GraphStyle 属性,我们绘制图形。
    • 步骤 7. 我们重新绘制轴,因为有些点可能位于轴上并可能覆盖它。

就这样!用 7 个简单步骤构建 GraphControl!!

运行代码

我使用 SharpDevelop 编写了此代码。如果您不使用 IDE,可以在命令提示符下直接编译 DemoForm.cs

  • 只需编译并运行演示项目,您就可以通过选择一个单选按钮,然后从组合框中选择一个示例颜色来更改颜色。
  • 您可以通过文本框和“添加”按钮添加新的 Points
  • 示例程序显示了 AddPoints 函数的不同重载方法。
  • 注释掉 Main 中的第一行,取消注释第二行,以查看另一个图形演示。
  • 如果您正在编写代码并希望在运行时操作 GraphControl 的行为,**不要忘记**调用控件的 Invalidate 函数。此函数强制控件重绘自身。

最后,我将 demowithclass.batdemowithdll.bat 文件与代码一起提供。第一个文件使用 graphcontrol.cs 文件和演示窗体。第二个文件构建了一个 graph.dll,演示窗体从 DLL 加载控件。

© . All rights reserved.