第5章,[Win32 章节] :在平面直角坐标系中描点

专栏导航

上一篇:第5章,Win32 章节 :绘制平面直角坐标系

回到目录

下一篇:无

本专栏课件

关于本专栏课件的获取方法,请参考下述课节。

参考课节:MFC笔记:本专栏课件

本节前言

对于本节所讲解的知识,有可能,你会需要时不时地参考本专栏的其它文章。真的遇到了需要参考之前的文章的知识点,请你自行查阅。

我呢,也会提到一部分的参考课节。但是呢,你不应该依赖于我的主动提及。最好呢,你自己能够多去了解和查看本专栏目录。

在上一节,我们编程绘制了一个平面直角坐标系。

本节呢,我们要去绘制一个复杂一点的平面直角坐标系,并且呢,我们还要在里面描几个点。

本节的篇幅比较长,建议你分成几次来阅读。

我们开始。

一. 本节代码

个人觉得,本节的代码量,是大一些的。所以呢,我先将代码给贴在下面,由大家先行浏览。随后,我会去讲解代码的逻辑。

代码如下。

复制代码
/*------------------------------------------------------------
   在平面直角坐标系中描点
                 水饺编程,2026
  ------------------------------------------------------------*/

#include <windows.h>
#include <tchar.h>

#define NUM_X_MARK  6
#define NUM_Y_MARK_HALF     3
#define NUM_CHAR    6

struct PointInfo
{
    POINT   pt;
    TCHAR   strPtName[NUM_CHAR];
    int     cbStrPtName;
};

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void DrawPoint(HDC hdc, struct PointInfo* pPointInfo,
    PPOINT pPtOrg, int nLenUnit, int nPtRadius);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("DrawPtInXOY");
    HWND         hwnd;
    MSG          msg;
    WNDCLASS     wndclass;

    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;

    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("注册窗口类失败!"),
            szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(szAppName,                  // window class name
        TEXT("在平面直角坐标系中描点"), // window caption
        WS_OVERLAPPEDWINDOW,        // window style
        CW_USEDEFAULT,              // initial x position
        CW_USEDEFAULT,              // initial y position
        CW_USEDEFAULT,              // initial x size
        CW_USEDEFAULT,              // initial y size
        NULL,                       // parent window handle
        NULL,                       // window menu handle
        hInstance,                  // program instance handle
        NULL);                     // creation parameters

    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    HDC         hdc;
    PAINTSTRUCT ps;
    COLORREF    color;
    HPEN    hPen, hOldPen;
    HBRUSH  hBrush, hOldBrush;
    TEXTMETRIC  tm;
    int     i, nCharWidth, nCharHeight;
    static POINT    ptOrigin;
    static int  nLenUnit, nLenArrow, nRadiusPt;
    static int  xLeft, xRight, yTop, yBottom;
    static POINT    x_mark[NUM_X_MARK][2];
    static POINT    y1_mark[NUM_Y_MARK_HALF][2];
    static POINT    y2_mark[NUM_Y_MARK_HALF][2];
    static POINT    ptXArrow[3], ptYArrow[3];
    static TCHAR    strXMark[NUM_X_MARK][NUM_CHAR];
    static TCHAR    strY1Mark[NUM_Y_MARK_HALF][NUM_CHAR];
    static TCHAR    strY2Mark[NUM_Y_MARK_HALF][NUM_CHAR];
    static int  cbStrXMark[NUM_X_MARK];
    static int  cbStrY1Mark[NUM_Y_MARK_HALF];
    static int  cbStrY2Mark[NUM_Y_MARK_HALF];
    static TCHAR    strOrigin[NUM_CHAR];
    static TCHAR    strXAxis[NUM_CHAR];
    static TCHAR    strYAxis[NUM_CHAR];
    static int  cbStrOrigin;
    static int  cbStrXAxis;
    static int  cbStrYAxis;
    static struct PointInfo MyPoint[5];

    switch (message)
    {
    case WM_CREATE:
        ptOrigin.x = 120;
        ptOrigin.y = 300;
        nLenUnit = 60;
        nLenArrow = 10;
        nRadiusPt = 4;

        xLeft = ptOrigin.x - nLenUnit;
        xRight = ptOrigin.x + nLenUnit * (NUM_X_MARK + 1);
        yTop = ptOrigin.y - nLenUnit * (NUM_Y_MARK_HALF + 1);
        yBottom = ptOrigin.y + nLenUnit * (NUM_Y_MARK_HALF + 1);

        for (i = 0; i < NUM_X_MARK; i++)
        {
            x_mark[i][0].x = ptOrigin.x + (i + 1) * nLenUnit;
            x_mark[i][0].y = ptOrigin.y - nLenArrow;
            x_mark[i][1].x = x_mark[i][0].x;
            x_mark[i][1].y = ptOrigin.y;
            cbStrXMark[i] = wsprintf(strXMark[i], _T("%d"), i + 1);
        }

        for (i = 0; i < NUM_Y_MARK_HALF; i++)
        {
            y1_mark[i][0].x = y2_mark[i][0].x = ptOrigin.x;
            y1_mark[i][0].y = ptOrigin.y - (i + 1) * nLenUnit;
            y2_mark[i][0].y = ptOrigin.y + (i + 1) * nLenUnit;
            y1_mark[i][1].x = y2_mark[i][1].x = ptOrigin.x + nLenArrow;
            y1_mark[i][1].y = y1_mark[i][0].y;
            y2_mark[i][1].y = y2_mark[i][0].y;
            cbStrY1Mark[i] = wsprintf(strY1Mark[i], _T("%d"), i + 1);
            cbStrY2Mark[i] = wsprintf(strY2Mark[i], _T("-%d"), i + 1);
        }

        cbStrOrigin = wsprintf(strOrigin, _T("O"));
        cbStrXAxis = wsprintf(strXAxis, _T("X 轴"));
        cbStrYAxis = wsprintf(strYAxis, _T("Y 轴"));

        ptXArrow[0].x = x_mark[NUM_X_MARK - 1][1].x + nLenUnit - nLenArrow;
        ptXArrow[0].y = x_mark[NUM_X_MARK - 1][1].y - nLenArrow;
        ptXArrow[1].x = x_mark[NUM_X_MARK - 1][1].x + nLenUnit;
        ptXArrow[1].y = x_mark[NUM_X_MARK - 1][1].y;
        ptXArrow[2].x = x_mark[NUM_X_MARK - 1][1].x + nLenUnit - nLenArrow;
        ptXArrow[2].y = x_mark[NUM_X_MARK - 1][1].y + nLenArrow;

        ptYArrow[0].x = y1_mark[NUM_Y_MARK_HALF - 1][0].x - nLenArrow;
        ptYArrow[0].y = y1_mark[NUM_Y_MARK_HALF - 1][0].y - nLenUnit + nLenArrow;
        ptYArrow[1].x = y1_mark[NUM_Y_MARK_HALF - 1][0].x;
        ptYArrow[1].y = y1_mark[NUM_Y_MARK_HALF - 1][0].y - nLenUnit;
        ptYArrow[2].x = y1_mark[NUM_Y_MARK_HALF - 1][0].x + nLenArrow;
        ptYArrow[2].y = y1_mark[NUM_Y_MARK_HALF - 1][0].y - nLenUnit + nLenArrow;

        MyPoint[0].pt.x = 3;
        MyPoint[0].pt.y = 1;
        MyPoint[0].cbStrPtName = wsprintf(
            MyPoint[0].strPtName, _T("A")
        );
        MyPoint[1].pt.x = 4;
        MyPoint[1].pt.y = 3;
        MyPoint[1].cbStrPtName = wsprintf(
            MyPoint[1].strPtName, _T("B")
        );
        MyPoint[2].pt.x = 1;
        MyPoint[2].pt.y = -2;
        MyPoint[2].cbStrPtName = wsprintf(
            MyPoint[2].strPtName, _T("C")
        );
        MyPoint[3].pt.x = 5;
        MyPoint[3].pt.y = 2;
        MyPoint[3].cbStrPtName = wsprintf(
            MyPoint[3].strPtName, _T("D")
        );
        MyPoint[4].pt.x = 5;
        MyPoint[4].pt.y = -1;
        MyPoint[4].cbStrPtName = wsprintf(
            MyPoint[4].strPtName, _T("E")
        );

        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        GetTextMetrics(hdc, &tm);
        nCharWidth = tm.tmAveCharWidth;
        nCharHeight = tm.tmHeight + tm.tmExternalLeading;

        MoveToEx(hdc, xLeft, ptOrigin.y, NULL);
        LineTo(hdc, xRight, ptOrigin.y);
        MoveToEx(hdc, ptOrigin.x, yTop, NULL);
        LineTo(hdc, ptOrigin.x, yBottom);

        Polyline(hdc, ptXArrow, 3);
        Polyline(hdc, ptYArrow, 3);

        for (i = 0; i < NUM_X_MARK; i++)
        {
            Polyline(hdc, x_mark[i], 2);
            TextOut(hdc, x_mark[i][1].x - nCharWidth / 2,
                x_mark[i][1].y + nLenArrow,
                strXMark[i], cbStrXMark[i]);
        }        

        for (i = 0; i < NUM_Y_MARK_HALF; i++)
        {
            Polyline(hdc, y1_mark[i], 2);
            TextOut(hdc, y1_mark[i][0].x - 2 * nCharWidth,
                y1_mark[i][0].y - nCharHeight / 2,
                strY1Mark[i], cbStrY1Mark[i]);
            Polyline(hdc, y2_mark[i], 2);
            TextOut(hdc, y2_mark[i][0].x - 3 * nCharWidth,
                y2_mark[i][0].y - nCharHeight / 2,
                strY2Mark[i], cbStrY2Mark[i]);
        }

        color = RGB(0, 0, 0);
        hPen = (HPEN)GetStockObject(NULL_PEN);
        hOldPen = (HPEN)SelectObject(hdc, hPen);
        hBrush = CreateSolidBrush(color);
        hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);
        Ellipse(hdc, ptOrigin.x - nRadiusPt,
            ptOrigin.y - nRadiusPt,
            ptOrigin.x + nRadiusPt,
            ptOrigin.y + nRadiusPt);
        SelectObject(hdc, hOldPen);
        SelectObject(hdc, hOldBrush);
        DeleteObject(hBrush);

        TextOut(hdc, ptOrigin.x - nCharWidth - nLenArrow,
            ptOrigin.y + nLenArrow, strOrigin, cbStrOrigin);
        TextOut(hdc, ptXArrow[2].x,
            ptXArrow[2].y + nLenArrow,
            strXAxis, cbStrXAxis);
        TextOut(hdc, ptYArrow[2].x + nLenArrow,
            ptYArrow[1].y,
            strYAxis, cbStrYAxis);

        for (i = 0; i < 5; i++)
            DrawPoint(hdc, &MyPoint[i], &ptOrigin,
                nLenUnit, nRadiusPt);

        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

void DrawPoint(HDC hdc, struct PointInfo* pPointInfo,
    PPOINT pPtOrg, int nLenUnit, int nPtRadius)
{
    HPEN    hPen, hOldPen;
    HBRUSH  hBrush, hOldBrush;
    COLORREF    color;
    POINT myPt;

    myPt.x = pPtOrg->x + pPointInfo->pt.x * nLenUnit;
    myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit;
    
    hPen = (HPEN)GetStockObject(NULL_PEN);
    hOldPen = (HPEN)SelectObject(hdc, hPen);
    color = RGB(0, 0, 255);
    hBrush = CreateSolidBrush(color);
    hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);

    Ellipse(hdc, myPt.x - nPtRadius,
        myPt.y - nPtRadius,
        myPt.x + nPtRadius,
        myPt.y + nPtRadius);
    SelectObject(hdc, hOldPen);
    SelectObject(hdc, hOldBrush);
    DeleteObject(hBrush);

    color = RGB(255, 0, 0);
    SetTextColor(hdc, color);
    TextOut(hdc, myPt.x + nPtRadius * 2,
        myPt.y + nPtRadius * 2, 
        pPointInfo->strPtName,
        pPointInfo->cbStrPtName);
}

本节代码,我将建立的解决方案与项目名称,取名为 DrawPtInXOY,源文件的名字为 WinMain.cpp 。

编译与运行方法,如果你忘了,可以参考下面的课节。

参考课节:使用 VS2010 编写 C语言程序

参考课节:用 VS2019 编写C语言程序

运行结果如下。
图1

在后续的分节段落中,我们来讲解代码的逻辑。

二. 平面直角坐标系的绘制

图1 中的内容,我们将其分为两部分。A、B、C、D、E 五个点及其文字标识,算作一部分,这部分我们将其称作描点部分。剩余部分为平面直角坐标系,它是平台部分,也是基础部分。

本分节,我们来讲解平台部分,也就是平面直角坐标系的绘制代码。

(一)变量声明与 WM_CREATE 消息中的代码

(1)原点、X 轴和 Y 轴

在构思的时候,我首先设定了原点,其声明如下所示。

复制代码
static POINT    ptOrigin;

在 WM_CREATE 消息中,我将其值初始化为(120,300)。

然后呢,我设定了坐标系中的单位1 。在我所绘制的坐标系里面,横纵坐标的单位1 的长度是一样的。关于单位1,我将其变量名取作 nLenUnit,在 WM_CREATE 消息中,我将它的值设置为 60 。

在 X 轴方面,我令 X 轴的左端点距离原点 1 个单位,令 X 轴右端点距离原点 7 个单位。原点左边的 X 轴上不设置刻度线,X 轴右边会设置刻度线。X 轴上的刻度线的数量为 6 。

我们在下述的宏定义中,定义了 X 轴 上的刻度线数目。

复制代码
#define NUM_X_MARK  6

上述代码块的内容位于程序的 9 号行中。

在 Y 轴方面,我令 Y 轴的上下端点,分别距离原点 4 个单位。原点之上的 Y 轴部分和原点之下的 Y 轴部分,分别有 3 个刻度线。

我们在下述的宏定义中,定义了 Y 轴的原点之上或者原点之下的刻度线数目。

复制代码
#define NUM_Y_MARK_HALF     3

索性,我们就将上述的两行代码的截图给贴出来,如下图所示。
图2

在图2 的行号 11 的位置,是本程序中的第三个宏定义代码。我们在程序中,需要标注 X 轴、Y 轴,也需要去标注原点与 A、B、C、D、E 五个点。这五个点的标注,都需要使用字符数组来表示。每个字符数组的元素个数,我用 NUM_CHAR 来表示,根据图3,它的值是 6 。当然,你可以自行修改这个值。

我声明了 int 型的 xLeft、xRight、yTop 和 yBottom 等四个变量,用来表示 X 轴左端点的横坐标、X 轴右端点的横坐标、Y 轴上端点的纵坐标和 Y 轴下端点的纵坐标。其声明和 WM_CREATE 消息中的初始化代码如下所示。

复制代码
static int  xLeft, xRight, yTop, yBottom;

xLeft = ptOrigin.x - nLenUnit;
xRight = ptOrigin.x + nLenUnit * (NUM_X_MARK + 1);
yTop = ptOrigin.y - nLenUnit * (NUM_Y_MARK_HALF + 1);
yBottom = ptOrigin.y + nLenUnit * (NUM_Y_MARK_HALF + 1);

(2)X 轴和 Y 轴的指示箭头

平面直角坐标系,除了表示 X 轴与 Y 轴的横线和竖线之外,还有用于表示正方向的箭头。为了绘制这个箭头,我们又声明了两个数组,声明代码如下。

复制代码
static POINT    ptXArrow[3], ptYArrow[3];

这两个数组,分别用于表示 X 轴上的箭头和 Y 轴上的箭头。由于每个箭头需要用三个点来标记,所以,每个数组,我们都将其设置为 3 个元素,每个元素,都是一个 POINT 结构的结构体变量。

为了绘制箭头,我设置了一个辅助变量,nLenArrow,并在 WM_CREATE 消息中,将其初始化为 10 。

由图1 可知,箭头的三个点中,有一个点,与 X 轴或 Y 轴正方向的端点重合。

以 X 轴为例,点 ptXArrow1 与 X 轴正方向的端点重合。点 ptXArrow0 是在 X 轴右端点的基础上,横坐标减去 nLenArrow,纵坐标也减去 nLenArrow 。点 ptXArrow2 是在 X 轴右端点的基础上,横坐标减去 nLenArrow,纵坐标加上 nLenArrow 。

以 Y 轴为例,点 ptYArrow1 与 Y 轴正方向的端点重合。点 ptYArrow0 是在 Y 轴上端点的基础上,横坐标减去 nLenArrow,纵坐标加上 nLenArrow 。点 ptXArrow2 是在 Y 轴上端点的基础上,横坐标加上 nLenArrow,纵坐标也加上 nLenArrow 。

(3)刻度线

然后呢,我们再来说说刻度线的事情。

X 轴上,我们设置了 6 个刻度线。这个 6,我们在代码中不是直接写 6,而是用宏代码 NUM_X_MARK 来表示的。

X 轴上的 6 个刻度线,每一个刻度线都需要用两个点来表示。一个点位于 X 轴,另一个点在 X 轴上方的位置。位于 X 轴上方的那个点,我们令其与 X 轴的距离为 nLenArrow 。

声明 X 轴刻度线的代码如下所示。

复制代码
static POINT    x_mark[NUM_X_MARK][2];

这是一个二维数组。由于每个刻度线需要两个点,所以,二维数组的右边的维度值,我将其设置为 2 。由于有 6 个刻度线,所以呢,我将左边的维度值设置为 NUM_X_MARK,也就是 6 。

以下是 X 轴上的 6 个刻度线的点的初始化代码。
图3

for 循环中,NUM_X_MARK 宏的值为 6,所以,for 循环执行了 6 次。每一次,都是初始化 1 条刻度线。每个刻度线包含有两个端点。其中,x_marki0 位于X 轴上方,距离 X 轴 nLenArrow 个像素。而 x_marki1 则是正好位于 X 轴。x_marki0 与 x_marki1,它们的 x 坐标相同,而纵坐标不同,x_marki0 的纵坐标等于 x_marki1 的纵坐标减去 nLenArrow 。

当 i 等于 0 时,我们绘制的是刻度为 1 的刻度线,当 i 等于 3 时,我们绘制的是刻度 4 的刻度线。在这种设置情况下,若是 119 行代码中,将(i + 1)改为 i,则 1 号刻度线就会与原点和 Y 轴正方向的一小段重合了。有了加 1 的设置,才会使得 i 为 0 时,设置的是 1 号刻度线。

123 行代码,它是用来初始化刻度文字的。当 i 为 0 时,我们设置的是 1 号刻度线,而在123 行中的 _T("%d") 的值为 i+1,为 1 。

当 i 为 4 时,我们设置的是 5 号刻度线,而在123 行中的 _T("%d") 的值为 i+1,为 5 。

格式化之后的刻度文字,会被存放在字符数组 strXMark 之中,并且,格式化之后的不包含 NULL 字符的有效字符数,会被 wsprintf 函数返回,并存放在 cbStrXMarki 之中。

在这里,也许,你忘了格式化函数 wsprintf 的用法了。我们给出几个参考课节的链接,如下所示。

参考课节:编程技能:格式化打印01,vsprintf 函数族简介

参考课节:编程技能:格式化打印02,vsprintf

参考课节:编程技能:格式化打印03,printf

参考课节:编程技能:格式化打印04,sprintf

参考课节:编程技能:格式化打印05,格式控制符

参考课节:第2章 :在 Windows 中使用 vsprintf 函数族

参考课节:第2章,标签 Win32 :格式化的消息框

参考课节:Windows 编程基础:wsprintf 函数

参考课节较多。如果,你之前没有学习过这些个知识,那么,你可算是有事做了。如果,你是一路跟着学习过来的,那么,你可以根据自己的情况,来选择性地复习部分的课节。

既然是谈到了 cbStrXMarki 和 strXMarki,我们也就索性介绍以下这两个数组。

strXMark 是一个二维数组,其中的每一行都是一个 TCHAR 字符数组,我们用它来标记 X 轴上的刻度线。cbStrXMark 数组,我们用它来表示对应的 strXMark 的某一行的 TCHAR 字符数组的有效字符数。

声明代码如下。

复制代码
static TCHAR    strXMark[NUM_X_MARK][NUM_CHAR];
static int  cbStrXMark[NUM_X_MARK];

至于这两个变量的初始化设置的代码,我们已经在图3 中的 for 循环代码中演示了。

这是对 X 轴的刻度线的处理了。

对 Y 轴的刻度线的处理,要略微复杂一点了。

我们将 Y 轴的刻度线,分为原点之上的部分和原点之下的部分。对应地,我们呢,设置了两组数组,用来表示刻度线,如下图所示。
图4

在图4 中,86 行声明的是原点之上的 Y 轴刻度线,87 行声明的是原点之下的 Y 轴刻度线。NUM_Y_MARK_HALF,它的值为 3 。原点之上的 Y 轴刻度线有 3 个,原点之下的 Y 轴刻度线也是 3 个,所以我将 y1_mark 和 y2_mark 的左边的维度值设置为 NUM_Y_MARK_HALF,为 3 。而每个刻度线需要两个点,所以我将右边的维度值设置为 2 。

与 X 轴刻度线类似,我们也对 Y 轴的刻度线设置了刻度文字字符数组和刻度文字有效字符数的数组,声明代码如下所示。
图5

对 Y 轴刻度线的初始化代码如下所示。
图6

NUM_Y_MARK_HALF 的值为 3,所以,图6 的 for 循环执行了 3 次,每次呢,都用来初始化原点之上的 i 号刻度线和原点之下的 i 号刻度线。

举例子来说,当 i 等于 0 时,初始化的是 1 和 -1 两个 Y 轴刻度线,它们分别是原点之上的 0 号刻度线和原点之下的 0 号刻度线,也就是原点之上的第 1 个刻度线和原点之下的第 1 个刻度线。其余的,依次类推。

对于 y1_mark 和 y2_mark,当右侧的维度值为 0 时,则对应的点是位于 Y 轴的那一点。当右侧的维度值为 1 时,它们是位于 Y 轴右边的点,与 Y 轴的距离是 nLenArrow 。

每一个刻度线的两点,其 y 坐标都是相同的,所以有 132 行和 133 行的代码。

129 和 130 行,都有(i + 1),而没有直接写成 i 。如果直接写成 i,则当 i 为 0 时,初始化的,就不是 1 和 -1 刻度线了,而是重合为原点与原点左侧的部分 X 轴的刻度了,那是不符合我们的意图的。

strY1Marki 是原点之上的 Y 轴刻度线的文字标识,strY2Marki 是原点之下的 Y 轴刻度线的文字标识。原点之上的标识,其值为正,所以,格式控制符用的是 _T("%d") 。原点之下的标识,其值为负,所以,格式控制符用的是 _T("-%d") 。

到了这里,我想,平面直角坐标系的基本的变量声明与初始化工作的代码,我差不多就算是讲完了。

(4)原点与坐标轴的文字标识

还有几个小尾巴,其中的一个小尾巴,是 X 轴、Y 轴和原点的标识,如下图所示。
图7

对于图7 中所涉及的变量,你猜都能猜到其含义的。

(5)坐标轴指示箭头的中间点的坐标代码

我们再来说一说 X 轴和 Y 轴箭头的初始化代码。
图8

我们前面说过了,ptXArrow1 位于 X 轴,所以,有145 行代码。145 行的 x_markNUM_X_MARK - 11 是什么?我们前面讲过了,NUM_X_MARK 的值为 6,则 NUM_X_MARK - 1 为 5,所以,x_markNUM_X_MARK - 1 是 5 号刻度线,刻度线索引号从 0 开始计数,所以呢,x_markNUM_X_MARK - 1 是 X 轴上从左数的第 6 个刻度线,是最右边的刻度线。

我们也讲过了,6 个 X 轴刻度线的两个点,当右边的维度值为 1 时,所表示的点是位于 X 轴的点。6 个刻度线的右侧维度为 1 的点,都位于 X 轴 。145 行所示的下述代码,

复制代码
ptXArrow[1].y = x_mark[NUM_X_MARK - 1][1].y;

其含义是,将 X 轴箭头的索引号为 1 的点的 y 坐标,设置为与 X 轴的最右边的刻度线上位于 X 轴的那个点的 y 坐标一样的值。

经此设置,则 X 轴箭头上索引号为 1 的点的 y 坐标,就与 X 轴的 y 坐标一样了,其实也与原点的 y 坐标一样。

我们再来看看 144 行代码。如下所示。

复制代码
ptXArrow[1].x = x_mark[NUM_X_MARK - 1][1].x + nLenUnit;

x_markNUM_X_MARK - 11.x 是 X 轴上最右边的刻度线的 x 坐标。那么,144 行代码中,在此 x 坐标的基础上又加上一个 nLenUnit 是怎么回事?

X 轴上有 6 个刻度线,从左数,第 1 个刻度线的 x 坐标等于原点的 x 坐标加上 1 × nLenUnit 。

第 1 个刻度线的 x 坐标等于原点的 x 坐标加上 1 × nLenUnit 。

第 3 个刻度线的 x 坐标等于原点的 x 坐标加上 3 × nLenUnit 。

第 6 个刻度线的 x 坐标等于原点的 x 坐标加上 6 × nLenUnit 。

而原点的 x 坐标加上 7 × nLenUnit,就是 xRight,是 X 轴的右端点的 x 坐标。

也就是,第 6 个刻度线的 x 坐标,再加上 1 个 nLenUnit,就是 xRight,就是 X 轴的右端点的 x 坐标。

第 6 个刻度线的 x 坐标,即为 x_markNUM_X_MARK - 11.x,它再加上 1 个 nLenUnit,就是 xRight,就是 X 轴的右端点的 x 坐标。

144 行代码的含义,我们讲完了。

151 行代码,

复制代码
ptYArrow[1].x = y1_mark[NUM_Y_MARK_HALF - 1][0].x;

它的含义与 145 行代码类似。

152 行代码,

复制代码
ptYArrow[1].y = y1_mark[NUM_Y_MARK_HALF - 1][0].y - nLenUnit;

它的含义与 144 行代码类似。

对于这两个类似的代码行,请大家自行分析。

绘制平面直角坐标系的变量声明与 WM_CREATE 消息中的初始化代码,我们也就算是讲完了。

(二)WM_PAINT 消息中的代码

(1)X 轴、Y 轴的横线和竖线与指示箭头

接下来,我们来看一看 WM_PAINT 消息中的代码。
图9

图9 中,191 行到 194 行,是用来绘制 X 轴、Y 轴的横线和竖线的代码。196 行和 197 行用于绘制箭头。

(2)获取当前设备环境的字体平均宽度与高度

图10

图10 的代码,用于获取当前设备环境中,所采用的字体的平均宽度和高度。它们对于绘制刻度文字是有用的。

(3)X 轴刻度线与标识

图11

图11 中,201 行的 Polyline 用于绘制刻度线,202 行的 TextOut 用于绘制刻度文字标识。在绘制刻度文字标识的时候,我们将文字的 y 坐标设置为 X 轴往下 1 个 nLenArrow 的位置,将文字的 x 坐标设置为对应的刻度线往左边半个平均字符宽度的位置。这样一来,刻度文字标识会正好以刻度线为 X 方向的中心。

(4)Y 轴刻度线与标识

图12

图12 用于绘制 Y 轴刻度线与文字标识。209 行和 213 行的 Polyline 用于绘制原点之上和原点之下的 Y 轴刻度线。210 行和 214 行的 TextOut 函数用于绘制原点之上和原点之下的 Y 轴刻度线文字标识。

在绘制刻度线文字标识的时候,原点之上的刻度文字标识的 x 坐标设置为 X 轴左边 2 个平均字符宽度的位置,原点之下的刻度文字标识的 x 坐标设置为 X 轴左边 3 个平均字符宽度的位置。之所以会有这种区别,那是因为,原点之下的刻度文字标识,都带有一个符号,所以,原点之下的文字标识,要往左边多一个平均字符宽度的位置。

不论是原点之上,还是原点之下,刻度文字标识的 y 坐标,都设置为对应刻度线往上半个字符高度的位置。这样一来,所绘制的刻度线文字标识,它的 Y 方向的中心,就是刻度线的 y 坐标了。

(5)给原点描点

图13

图13 所示代码,用来给平面直角坐标系的原点描点。所描的点,是一个正圆。这个正圆的左右和上下,分别是原点的横坐标往左往右 1 个 nRadiusPt 像素单位和原点的纵坐标往上往下 1 个 nRadiusPt 像素单位。

描点时,画笔用备用画笔 NULL_PEN,画刷用黑色画刷。描完了点以后,还要记得将画笔画刷选出设备环境,画笔不需要删除,而画刷是自定义对象,需要删除。

(5)绘制原点和坐标轴的文字标识

图14

图14 所示的代码,用于绘制原点、X 轴和 Y 轴的文字标识。原点用大写的英文字母 O 来标识。

到了这里,平面直角坐标系的绘制代码,我们就讲完了。

三. 在坐标系中描点

这一部分的内容,实际上,是我们设置本课节的核心用意所在,是本课节的重点,请大家重点研究。

(一)PointInfo 结构体

我们来看一看这个结构体的代码。
图15

为了描点,我们需要记录一个点在平面直角坐标系中的坐标。这个坐标,我们用 POINT 结构的 pt 变量来表示。

我们也需要有一个数据结构,来记录这个点的名称,以便在平面直角坐标系中标记这个点。这个数据结构,就是 16 行的 strPtName 字符数组。在这里,我们没有使用 C++ 代码。以后,在学习 MFC 的时候,我们其实是有着更好的表示方式的,那就是 CString 类型的变量。这是后话,以后,我们学习 MFC 时,自会见到它和使用它。而在学习 Windows API 程序的时候,我们只使用字符数组,尤其是 TCHAR 类型的字符数组。

我们还需要一个整型变量,用来记录字符数组中,存储的字符串的有效字符数,这个有效字符数不包含结尾的 NULL 字符。

需要注意的是,实际上,我这种声明结构体类型的方式,它是不符合 Windows 程序设计的惯例的。

按照惯例,更好的声明方式如下面的代码块所示。

复制代码
typedef struct tagPOINTINFO
{
    POINT   pt;
    TCHAR   strPtName[NUM_CHAR];
    int     cbStrPtName;
} POINTINFO, *PPOINTINFO;

写代码时,忘了这一茬,以后,我会多注意的。也请大家在写 Windows 程序的时候,多注意遵循 Windows 程序设计的惯例。

(二)对 PointInfo 结构体变量的初始化

我们在程序中,初始化了五个点,变量声明如下面的代码块所示。

复制代码
static struct PointInfo MyPoint[5];

我们在 WM_CREATE 消息中初始化了五个点,这些个初始化代码如下面的代码块所示。

复制代码
MyPoint[0].pt.x = 3;
MyPoint[0].pt.y = 1;
MyPoint[0].cbStrPtName = wsprintf(
        MyPoint[0].strPtName, _T("A")
    );
MyPoint[1].pt.x = 4;
MyPoint[1].pt.y = 3;
MyPoint[1].cbStrPtName = wsprintf(
        MyPoint[1].strPtName, _T("B")
    );
MyPoint[2].pt.x = 1;
MyPoint[2].pt.y = -2;
MyPoint[2].cbStrPtName = wsprintf(
        MyPoint[2].strPtName, _T("C")
    );
MyPoint[3].pt.x = 5;
MyPoint[3].pt.y = 2;
MyPoint[3].cbStrPtName = wsprintf(
        MyPoint[3].strPtName, _T("D")
    );
MyPoint[4].pt.x = 5;
MyPoint[4].pt.y = -1;
MyPoint[4].cbStrPtName = wsprintf(
        MyPoint[4].strPtName, _T("E")
    );

这五个点的名称分别为 A、B、C、D、E,它们的坐标分别为(3,1)、(4,3)、(1,-2)、(5,2)和(5,-1)。

我们都知道,原点的坐标为(0,0)。那么,接下来,我们又该如何在平面直角坐标系中,描绘这些个点呢?

请看下一分节。

(三)描点函数与描点

(1)描点函数的声明

首先呢,我们来看一看描点函数的声明。

复制代码
void DrawPoint(HDC hdc, struct PointInfo* pPointInfo,
    PPOINT pPtOrg, int nLenUnit, int nPtRadius);

描点函数的名字叫做 DrawPoint,它有五个参数。

第一个参数,是设备环境句柄变量。设备环境句柄类型为 HDC,形参变量为 hdc 。由于 DrawPoint 函数将会在 WM_PAINT 消息中被调用,因此,传入的 hdc,实际上是 WM_PAINT 消息中的那个 hdc 。

第二个参数,是指向 struct PointInfo 结构体类型的结构体指针变量,pPointInfo 。

第三个参数,是指向 POINT 结构体类型的指针变量,pPtOrg 。pPtOrg变量,实际上是关于原点这个点的变量的指针。在窗口过程函数 WinProc 中,我们将原点声明为 ptOrigin,所以,实际上,传入的实参是 &ptOrigin 。

在这里,我们这就看一看 WM_PAINT 消息中的调用 DrawPoint 函数的代码。
图16

第四个参数,nLenUnit,它是平面直角坐标系中,单位1 所对应的像素数。

第五个参数,nPtRadius,它是描点时所使用的圆的半径。我们在第一个参数中,给出了所要描绘的点的坐标,它将作为描点的圆心,本参数给出的半径,旨在让第一个参数所指示的点能够清晰地展现出来,同时又不至于太大。第一个参数所给出的点,它往上下左右分别移动本参数所指示的半径的像素距离,就能够给出描点所在的圆的范围了。

(2)DrawPoint 函数实现

五个参数和调用 DrawPoint 函数的代码我们大致说完了,接下来,我们来看一看 DrawPoint 函数的具体代码。

复制代码
void DrawPoint(HDC hdc, struct PointInfo* pPointInfo,
    PPOINT pPtOrg, int nLenUnit, int nPtRadius)
{
    HPEN    hPen, hOldPen;
    HBRUSH  hBrush, hOldBrush;
    COLORREF    color;
    POINT myPt;

    myPt.x = pPtOrg->x + pPointInfo->pt.x * nLenUnit;
    myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit;
    
    hPen = (HPEN)GetStockObject(NULL_PEN);
    hOldPen = (HPEN)SelectObject(hdc, hPen);
    color = RGB(0, 0, 255);
    hBrush = CreateSolidBrush(color);
    hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);

    Ellipse(hdc, myPt.x - nPtRadius,
        myPt.y - nPtRadius,
        myPt.x + nPtRadius,
        myPt.y + nPtRadius);
    SelectObject(hdc, hOldPen);
    SelectObject(hdc, hOldBrush);
    DeleteObject(hBrush);

    color = RGB(255, 0, 0);
    SetTextColor(hdc, color);
    TextOut(hdc, myPt.x + nPtRadius * 2,
        myPt.y + nPtRadius * 2, 
        pPointInfo->strPtName,
        pPointInfo->cbStrPtName);
}

接下来,我们先来看变量声明部分。
图17

这几行代码,声明了画笔、画刷句柄变量,颜色变量,还有一个 POINT 类型的变量。

注意,这个 POINT 类型的 myPt 变量,它所要构建的,不是平面直角坐标系中的坐标。描点时,我们需要将平面直角坐标系中的点的坐标转化为客户区中的点的坐标。而 myPt 变量所要构建的,正是转化之后的客户区中的点的坐标。

我们接着往下看。

复制代码
myPt.x = pPtOrg->x + pPointInfo->pt.x * nLenUnit;
myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit;

这两行代码,在于将传入的平面坐标系中的点的坐标,转化为点在客户区中的坐标。我们暂且不细讲,我们先往下看。
图18

图18 所示的代码,就是描点的代码了。266 到 270 行代码,它们的作用是获取系统备用画笔 NULL_PEN 并将画笔保存在 hPen 里面和建立自定义的蓝色画刷,并将画笔和画刷分别选入设备环境之中。注意,选入设备环境之时,所使用的设备环境句柄 hdc,是从 WM_PAINT 消息中传递过来的,本质上,它与 WM_PAINT 消息中的设备环境句柄是同一个。

然后呢,272 行到 275 行是描点代码。我们想要将点给清晰地展示出来,那就需要调用椭圆函数 Ellipse,以传过来的第一个参数作为圆心,以传过来的第五个参数为半径,来绘制一个正圆,来表现这个点。

最后呢,276 到 278 行,则是用来将完成了绘图使命的 GDI 对象给选出设备环境以及删除 GDI 对象的代码。hPen 中的画笔是备用画笔,不可以删除。而 hBrush 中的画刷是自定义画刷,因而需要删除。

以上为描点代码,我们再往下看。
图19

这一部分的代码,它们是绘制点所对应的标记文字的。也就是绘制 A、B、C、D、E 这些标识文字。

280 行设置 color 变量,它将作为标识文字的颜色。281 行调用 SetTextColor 函数,设置文字颜色。282 行绘制标识文字。绘制标识文字的时候,我们让文字位于点右边两个半径的位置,位于点下边两个半径的位置。

基本的描点框架代码,我们就讲完了。

(3)从平面直角坐标系到客户区坐标系的 x 坐标转化方法

接下来,我们来看一看转换坐标的两行代码。
图20

263 行代码,是对平面直角坐标系的点的 x 坐标,转化为客户区坐标系的 x 坐标。

方法是,将传入的点的平面直角坐标系的 x 坐标乘以单位1 所对应的像素数,所得的乘积加上原点所对应的客户区坐标系的 x 坐标。

经过查询,nLenUnit,它的值是 60 。也就是,平面直角坐标系的单位1,对应着客户区坐标的 60 个像素。而原点的客户区坐标,为(120,300)。

也就是说,如果某个平面直角坐标系中的点,它的坐标为(9,10),则它的横坐标 9,要乘以 60,然后加上 120,结果为 660,此点绘制在客户区坐标系中,客户区的 x 坐标为 660 号像素列。

在这里,平面直角坐标系中的 x 坐标乘以单位1 对应的像素数,这个我们不需要太关注。需要关注的是,【pPtOrg->x】和【pPointInfo->pt.x * nLenUnit】之间,是以加号来连接的。

这个是因为,在客户区坐标系中,越往右, x 坐标越大;越往左,x 坐标越小。这与平面直角坐标系的规则是相同的。

我们在中小学以及大学里面,所学习的平面直角坐标系,都是说,X 轴的正方向在右边。越往右,x 坐标越大,越往左, x 坐标越小。

在本节程序中,在 DrawPoint 函数中,第三个形式参数 pPtOrg,它其实是一个参考点的指针。在本节程序中,我们规定,这个参考点,是原点(0,0)。

我们来计算一下,平面指教坐标系中的(3,1)对应的客户区坐标系中的 x 坐标。根据 263 行代码,mtPt.x = 120 + 3 × 60 = 120 + 180 = 300 。所以呢,平面指教坐标系中的(3,1)对应的客户区坐标系中的 x 坐标 是 300 号像素列。

实际上,我们除了将平面直角坐标系中的原点(0,0)设置为参考点之外,完全可以设置其他的参考点,比如说,我们可以将平面直角坐标系的(3,1)作为参考点。然后呢,(3,1)这个点的客户区坐标为(300,240)。

在以平面直角坐标系的点(3,1)为参考点的情况下,我们来计算平面直角坐标系的点(6,7)在客户区坐标系中的 x 坐标。

参考点(3,1)与目标点(6,7)的 x 坐标相差为 6-3,为 3 。而 nLenUnit 所代表的单位1,是 60 像素。所以,参考点(3,1)与目标点(6,7),在客户区坐标系中会相差 60×3 个像素,也就是相差 180 个像素。由于客户区坐标系与平面直角坐标系一样,都是越往右 x 越大,所以呢,由于从参考点(3,1)变到(6,7),在 x 坐标方面需要加上某一个正数,因而从参考点(3,1)对应的客户区坐标(300,240)变到目标点(6,7)对应的客户区坐标,在 x 坐标方面也需要加上某一个正数。所加的正数,就是参考点和目标点所差的像素数,180,而 300 + 180 = 480,所以,目标点(6,7)对应的客户区坐标的 x 坐标,为 480 。

仍然是以平面直角坐标系的点(3,1)作为参考点,这个参考点所对应的客户区坐标依然是(300,240),平面直角坐标系中的单位1 依然对应着客户区坐标的 60 个像素。然后呢,这次,我们来计算平面直角坐标系的点(16,3)所对应的客户区坐标中的 x 坐标。

目标点(16,3)相比参考点(3,1),在 x 方面的增值为 16-3,为 13,所以,目标点和参考点相差的像素数,为 13 × 60,为 780 个像素。由于客户区坐标系中,x 坐标的规则同平面直角坐标系一样,越往右越大,越往左越小,所以呢,由于从参考点(3,1)变化到目标点(16,3)需要加上某一个正数(这个正数其实是 13),因而,对应到客户区坐标系中,参考点(3,1)所对应的客户区坐标(300,240)中的 x 坐标 300,也需要加上一个正数,这个正数就是 780 。所以,目标点(16,3)对应的客户区坐标的 x 坐标,为 300 + 780,为 1080 。

不知道,你是否看懂了。多看一看,多思考一下吧。这一块,应该是不难。不过,我这会儿,写教程的时候,则是感到了一阵晕,不好讲啊。

(4)从 XOY 坐标系到客户区坐标系的 y 坐标转化方法

不好讲,还是得讲。
图20 副本

我们接着来看图20 中的 264 行代码。

它是用来将平面直角坐标系中某个点的 y 坐标转化为客户区坐标系中的 y 坐标,所使用的转换代码。

方法是,将平面直角坐标系中某个点的 y 坐标乘以单位1 所对应的 60 像素,然后用原点(0,0)所对应的客户区坐标中的 y 坐标,减去之前的那个乘积。在这里,nLenUnit 就是平面直角坐标系的单位1 所对应的客户区坐标的像素数,60 。pPointInfo->pt.y 就是平面指教坐标系中某个点的 y 坐标,它是 DrawPoint 函数接收的第一个实参的提取与应用。

在这里呢,有趣的地方在于,pPtOrg->y 和 pPointInfo->pt.y * nLenUnit 之间,是以减号来连接。

为啥是这个样子呢?这是因为,在平面直角坐标系中,越往上 y 坐标越大,越往下 y 坐标越小,而在客户区坐标系中与此相反,越往下 y 坐标越大,越往上 y 坐标越小。由于在平面直角坐标系和客户区坐标系中,y 坐标的变化规则相反,因而,pPtOrg->y 和 pPointInfo->pt.y * nLenUnit 之间,是以减号来连接的。

已知平面指教坐标系中的原点(0,0)所对应的客户区坐标为(120,300),求平面直角坐标系中的点(3,1)在客户区坐标系中的纵坐标。

根据 264 行代码,300 - 1 × 60 = 300 - 60 = 240,所以呢,平面直角坐标系中的点(3,1)在客户区坐标系中的 y 坐标为 240 。

实际上,平面直角坐标系中的点(3,1)对应的客户区坐标的确是(300,240)。那么,我们就尝试以平面直角坐标系的点(3,1)为参考点,平面直角坐标系中的单位1 对应着客户区坐标系的 60 个像素,以此为背景设定,来求平面直角坐标系中的点(4,3)、(1,-2)、(5,2)和(5,-1)所对应的客户区坐标中的 y 坐标。

a. 计算 XOY 坐标系(4,3)对应的客户区 y 坐标

首先看(4,3)。参考点(3,1)变到(4,3),y 坐标方面的增量为 3-1,为 2 。这对应的客户群像素距离为 2 × 60,为 120 个像素。由于参考点(3,1)变到目标点(4,3)需要加上一个正数,2,又由于平面直角坐标系与客户区坐标系中 y 的变化规则相反,所以呢,参考点(3,1)对应的客户区坐标中的 y 坐标 240,需要减去一个正数,才能变化到目标点(4,3),而这个需要减去的正数,为 120,所以,目标点(4,3)所对应的客户区坐标的 y 值,为 240-120,为 120 。

我们用 264 行的公式来重新算一次。

myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit

= 300 - 3 × 60 = 300 - 180 = 120

所以,目标点(4,3)对应的客户区坐标中的 y 坐标,为 120 。

b. 计算 XOY 坐标系(1,-2)对应的客户区 y 坐标

我们再来看(1,-2),我们同样是先以(3,-1)作为参考点。平面直角坐标系中的点(3,-1)对应的客户区坐标为(300,240),平面直角坐标系中的单位1 对应着客户区坐标的 60 个像素。我们在下面开始来进行着运算。

参考点(3,1)变化到(1,-2),y 坐标的增量为 -2-1,为 -3 。别管它是 +3 还是 -3,总之绝对值是 3 。平面直角坐标系中的绝对值变化量 3,对应到客户区坐标系中,为 3 × 60,为 180 个像素。由于从参考点(3,1)变化到(1,-2),y 方向需要减去一个正数, 3,又由于平面直角坐标系和客户区坐标系的 y 坐标变化规则相反,所以,参考点(3,1)所对应的客户区坐标(300,240)的纵坐标 240,需要加上一个正数,以变化到目标点(1,-2)所对应的客户区坐标的 y 坐标。这个需要加上的正数,为 180,所以呢,目标点(1,-2)对应的客户区坐标的 y 坐标,为 240 + 180,为 420 。

在这里呢,我们来换一个思路,将参考点(3,1)的客户区纵坐标变化到目标点(1,-2)的客户区纵坐标的过程给整理一下。

参考点(3,1)变化到(1,-2),y 坐标的增量为 -2-1,为 -3 。平面直角坐标系中的增量变化 -3,对应到客户区坐标系中的增量,为 -3 × 60,为 -180 个像素。由于从参考点(3,1)变化到(1,-2),y 方向需要加上增量, -3,又由于平面直角坐标系和客户区坐标系的 y 坐标变化规则相反,所以,参考点(3,1)所对应的客户区坐标(300,240)的纵坐标 240,需要减去增量,以变化到目标点(1,-2)所对应的客户区坐标的 y 坐标。这个需要减去的增量,为 -180,所以呢,目标点(1,-2)对应的客户区坐标的 y 坐标,为 240 - (-180),为 420 。

然后呢,我们再以 264 行代码来计算(1,-2)对应的客户区 y 坐标。

myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit

= 300 - (-2)× 60 = 300 - (-120)= 300 + 120 = 420

与我们以(3,1)为参考点来计算的 y 坐标值相同。

c. 计算 XOY 坐标系(5,2)对应的客户区 y 坐标

有了这种不计正负号的增量方法以后,我们再来看看平面直角坐标系中的两个目标点(5,2)和(5,-1)的计算。

我们还是以参考点(3,1)来进行计算。参考点(3,1)对应的客户区坐标为(300,240),平面直角坐标系的单位1 对应的客户区像素数为 60 个像素。

我们来看平面直角坐标系的目标点(5,2)。

从参考点(3,1)变化到目标点(5,2),y 坐标的增量为 2-1,为 1 。平面直角坐标系中的增量变化 1,对应到客户区坐标系中的增量,为 1 × 60,为 60 个像素。由于从参考点(3,1)变化到(5,2),y 方向需要加上增量, 1,又由于平面直角坐标系和客户区坐标系的 y 坐标变化规则相反,所以,参考点(3,1)所对应的客户区坐标(300,240)的纵坐标 240,需要减去增量,以变化到目标点(5,2)所对应的客户区坐标的 y 坐标。这个需要减去的增量,为 60,所以呢,目标点(5,2)对应的客户区坐标的 y 坐标,为 240 - 60,为 180 。

然后呢,我们再以 264 行代码来计算(5,2)对应的客户区 y 坐标。

myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit

= 300 - 2 × 60 = 300 - 120 = 180

与我们以(3,1)为参考点来计算的 y 坐标值相同。

d. 计算 XOY 坐标系(5,-1)对应的客户区 y 坐标

最后,我们来看一看平面直角系的目标点(5,-1)。

从参考点(3,1)变化到目标点(5,-1),y 坐标的增量为 -1-1,为 -2 。平面直角坐标系中的增量变化 -2,对应到客户区坐标系中的增量,为 -2 × 60,为 -120 个像素。由于从参考点(3,1)变化到(5,-1),y 方向需要加上增量, -2,又由于平面直角坐标系和客户区坐标系的 y 坐标变化规则相反,所以,参考点(3,1)所对应的客户区坐标(300,240)的纵坐标 240,需要减去增量,以变化到目标点(5,-1)所对应的客户区坐标的 y 坐标。这个需要减去的增量,为 -120,所以呢,目标点(5,-1)对应的客户区坐标的 y 坐标,为 240 - (-120),为 360 。

然后呢,我们再以 264 行代码来计算(5,2)对应的客户区 y 坐标。

myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit

= 300 - (-1)× 60 = 300 - (-60)= 300 + 60 = 360

与我们以(3,1)为参考点来计算的 y 坐标值相同。

(5)重新梳理从平面直角坐标系到客户区坐标系的 x 坐标转化方法

a. 重新梳理目标点(6,7)的 x 坐标转化计算

在以平面直角坐标系的点(3,1)为参考点,来计算平面直角坐标系的点(6,7)在客户区坐标系中的 x 坐标的时候,我们的说法如下所示。

参考点(3,1)与目标点(6,7)的 x 坐标相差为 6-3,为 3 。而 nLenUnit 所代表的单位1,是 60 像素。所以,参考点(3,1)与目标点(6,7),在客户区坐标系中会相差 60×3 个像素,也就是相差 180 个像素。由于客户区坐标系与平面直角坐标系一样,都是越往右 x 越大,所以呢,由于从参考点(3,1)变到(6,7),在 x 坐标方面需要加上某一个正数,因而从参考点(3,1)对应的客户区坐标(300,240)变到目标点(6,7)对应的客户区坐标,在 x 坐标方面也需要加上某一个正数。所加的正数,就是参考点和目标点所差的像素数,180,而 300 + 180 = 480,所以,目标点(6,7)对应的客户区坐标的 x 坐标,为 480 。

现在,我们以增量表述方法,重新整理上述说法,整理结果如下。

从参考点(3,1)变化到目标点(6,7), x 坐标的增量为 6-3,为 3 。而 nLenUnit 所代表的单位1,是 60 像素。平面直角坐标系中的增量 3,对应到客户区坐标系中的增量,为 3 × 60,为 180 个像素。由于客户区坐标系与平面直角坐标系的 x 坐标变化规则相同,都是越往右 x 越大,所以呢,由于从参考点(3,1)变到(6,7),在 x 坐标方面需要加上增量,3,因而,从参考点(3,1)对应的客户区坐标(300,240)的 x 坐标 300,变化到目标点(6,7)对应的客户区坐标的 x 坐标,在 x 坐标方面也需要加上一个增量。所加的增量,就是 180,而 300 + 180 = 480,所以,目标点(6,7)对应的客户区坐标的 x 坐标,为 480 。

b. 重新梳理目标点(16,3)的 x 坐标转化计算

在以平面直角坐标系的点(3,1)为参考点,来计算平面直角坐标系的点(16,3)在客户区坐标系中的 x 坐标的时候,我们的说法如下所示。

目标点(16,3)相比参考点(3,1),在 x 方面的增值为 16-3,为 13,所以,目标点和参考点相差的像素数,为 13 × 60,为 780 个像素。由于客户区坐标系中,x 坐标的规则同平面直角坐标系一样,越往右越大,越往左越小,所以呢,由于从参考点(3,1)变化到目标点(16,3)需要加上某一个正数(这个正数其实是 13),因而,对应到客户区坐标系中,参考点(3,1)所对应的客户区坐标(300,240)中的 x 坐标 300,也需要加上一个正数,这个正数就是 780 。所以,目标点(16,3)对应的客户区坐标的 x 坐标,为 300 + 780,为 1080 。

我们采用增量的概念,重新梳理上述的内容,梳理的结果如下。

从参考点(3,1)到目标点(16,3),在 x 方面的增值为 16-3,为 13。平面直角坐标系中的增量 13,对应到客户区坐标系中的增量,为 13 × 60,为 780 个像素。由于客户区坐标系与平面直角坐标系的 x 坐标变化规则相同,都是越往右 x 越大,所以呢,由于从参考点(3,1)变到(16,3),在 x 坐标方面需要加上增量,13,因而,从参考点(3,1)对应的客户区坐标(300,240)的 x 坐标 300,变化到目标点(16,3)对应的客户区坐标的 x 坐标,在 x 坐标方面也需要加上一个增量。所加的增量,就是 780,而 300 + 780 = 1080,所以,目标点(16,3)对应的客户区坐标的 x 坐标,为 1080 。

c. 目标点(-7,6)的 x 坐标转化计算

有了增量的概念,我们重新来计算一个新的 x 坐标。这次,我们来计算的是平面直角坐标系中的点(-7,6)转化为客户区坐标以后的 x 坐标。

从参考点(3,1)到目标点(-7,6),在 x 方面的增值为 -7-3,为 -10。平面直角坐标系中的增量 -10,对应到客户区坐标系中的增量,为 -10 × 60,为 -600 个像素。由于客户区坐标系与平面直角坐标系的 x 坐标变化规则相同,都是越往右 x 越大,所以呢,由于从参考点(3,1)变到(-7,6),在 x 坐标方面需要加上增量,-10,因而,从参考点(3,1)对应的客户区坐标(300,240)的 x 坐标 300,变化到目标点(-7,6)对应的客户区坐标的 x 坐标,在 x 坐标方面也需要加上一个增量。所加的增量,就是 -600,而 300 + (-600) = -300,所以,目标点(-7,6)对应的客户区坐标的 x 坐标,为 -300 。

接下来,我们重新贴一下图20 。
图20 副本

接下来,我们再用图20 中的 263 行的代码,来计算一下目标点(-7,6)转化到客户区坐标系以后的 x 坐标。

myPt.x = pPtOrg->x + pPointInfo->pt.x * nLenUnit

= 120 + (-7)× 60 = 120 + (-420)= 120 - 420 = -300

这与以(3,1)为参考点计算的结果是相同的。

到了这里,从平面直角坐标系到客户区坐标系的 x 坐标和 y 坐标的转化方法,我们就算是讲完了。

四. 坐标转化小结

在坐标系的转化方面,在 X 坐标方面,平面直角坐标系与客户区坐标系的变化规则是相同的,都是越往右 x 越大,越往左 x 越小。由于 x 坐标在两种坐标系之中变化规则相同,所以,在转化时,代码【myPt.x = pPtOrg->x + pPointInfo->pt.x * nLenUnit;】之中,【pPtOrg->x】和【pPointInfo->pt.x * nLenUnit】之间的连接符号,是加号。

在 Y 坐标方面,平面直角坐标系与客户区坐标系的变化规则是相反的。在平面直角坐标系之中,越往上 y 越大,越往下 y 越小。而在客户区坐标系之中,越往下 y 越大,越往上 y 越小。由于 y 坐标在两种坐标系之中变化规则相反,所以,在转化时,代码【myPt.y = pPtOrg->y - pPointInfo->pt.y * nLenUnit;】之中,【pPtOrg->y】和【pPointInfo->pt.y * nLenUnit】之间的连接符号,是减号。

也就是,在两种坐标系的转化之中,X 坐标变化规则相同,所以 X 坐标的转化用加号【+】;Y 坐标的变化规则相反,所以 Y 坐标的转化用减号【-】

这种转化规则,是本节的重点。

后面,我们在讲解 SineWave 程序的时候,这种 X 坐标和 Y 坐标在平面直角坐标系与客户区坐标系之间转化的运算方法,仍然会需要用到本节的知识。

对于这种 X、Y 坐标在两种坐标系之间的转化方法,我觉得,我的整理,似乎还是比较乱。

但是,我还是希望你在我的这种反复举例之中,你能够理解它。

或许,你可以找到比我的表述方法更好的方式。

如果你有更好的表述方法,欢迎你告诉我。

结束语

本节的内容,可真的是有点长啊。

我自己也觉得写得累。暂时地,我是不想重新写和修改本文了。以后有机会,再去改吧。

专栏导航

上一篇:第5章,Win32 章节 :绘制平面直角坐标系

回到目录

下一篇:无

相关推荐
linx2951 小时前
单元九 · 零基础路线图-第 7–9 周·类与 RAII
c语言·开发语言·数据结构·c++·嵌入式硬件·算法
无忧.芙桃1 小时前
嵌入式(一):嵌入式历史与计算机基础:从 ENIAC 到 MCU 四大技术方向
c语言·单片机·嵌入式硬件·计算机
布莱克6051 小时前
C++ 七大排序算法详解:选择、冒泡、插入、计数、堆、快速与归并排序
数据结构·c++·算法·排序算法
一只旭宝1 小时前
Python 与 C/C++ 内存模型对比总结
c语言·c++·python
心态还需努力呀2 小时前
把 Flameshot 编译进鸿蒙 PC:一次 Qt C++ 截图工具的源码级移植实战
c++·qt·harmonyos
白色的北极熊2 小时前
c语言 输出菱形字符串
c语言·开发语言
科学实验家2 小时前
最小生成树:Prim,kruskal
数据结构·c++·算法
学习智者2 小时前
《玄》IDE v3.6.3重磅发布:全功能修复与性能飞跃
开发语言·c++·ide·算法·中文语言 玄
2402_882893862 小时前
C++异常:从概念到实践的一篇博客
c++·异常