×
Featured image of post 换个姿势学C语言第8章

换个姿势学C语言第8章

换个姿势学C语言 第8章 图形编程基础

0. 说明

《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

Snipaste_2024-03-10_14-51-10.png

这是一本非常不错的书!

8. 图形编程基础

上一章节我们已经通过GetAllRates接口获取到了所有的外汇牌价信息,并保存到文件当中,并能在控制台输出中显示出来。但外汇牌价看板是一个图形用户界面的程序。下面是第一章给出的效果图:

Snipaste_2024-03-10_00-12-26.png

这种包含图形元素的程序外观被称为“图形用户界面”(Graphical User Interface, GUI)。相对于纯文本组成的用户界面,对于用户来说,图形用户界面在视觉上更易于接受,也更容易操作。

8.1 图像在计算机中的表示

早期的计算机只能打印或显示文本,为了使计算机能够显示“图形”,最早的方法是使用多行的ASCII文本来表达图形,这种方式被称为“字形画”,如下面这个字符:

1
2
3
4
5
6
7
      .o.        .oooooo..o   .oooooo.   ooooo ooooo 
     .888.      d8P'    `Y8  d8P'  `Y8b  `888' `888' 
    .8"888.     Y88bo.      888           888   888  
   .8' `888.     `"Y8888o.  888           888   888  
  .88ooo8888.        `"Y88b 888           888   888  
 .8'     `888.  oo     .d8P `88b    ooo   888   888  
o88o     o8888o 8""88888P'   `Y8bood8P'  o888o o888o   

你可以在 ASCII文本艺术图像在线生成器 https://www.perfcode.com/tools/generator/ascii-art 生成你需要的字形画!

随着计算机技术的发展,图形显示卡、图形显示器的普及使得显示图像成为可能。

在开始编写显示图像的程序之前,我们先来了解各种图片是如何输入计算机中的。

我们将计算机中的图分为两类:

  • 图形:用点、直线或者多边形等几何图元素表示的图片,一般来自人工绘制。
  • 图像:由扫描仪、数码相机、摄像机等输入设备捕捉实际画面产生的数字图像。
8.1.1 来自人工绘制的矢量图形

图形来自人工绘制,例如平面设计师使用CorelDRAW进行广告设计,工程师使用AutoCAD设计机械部件。绘制的工具一般是鼠标,有时也可以使用绘图板。这种图形一般由各种点、线和多边线组成,它们被称为矢量图形。

这种图形可以拆解成很多个点、线和多边形,每一个缅怀部分都可以用基于数学方程的几何图元表示,绘图时只要描述各种形状和填充颜色即可,用这种方式描述图形的好处是文件体积比较小,而且使用专门的软件放大显示并不会影响清晰度(不会出现锯齿或马赛克)。

8.1.2 来自数据相机或扫描仪的位图
  • 矢量图形并不适合描述来自真实世界的景象,因此它无法精确还原每一处细节。

  • 来自现实世界的照片通常使用数码相机拍摄,或者使用胶片相机拍摄后再用扫描仪等设备输入计算机。

  • 图像传感器是一种矩形集成电路,在图像传感器上按矩形排列着大量感光元件,将画面分隔成若干个栅格,一个栅格被称为一个像素。

  • 使用三种基本颜料红(Red)、绿(Green)和蓝(Blue)以不同比例组合到一起调配出各种颜色。

  • 显示器在显示图片时采用同样的原理:将红绿蓝三原色的色光以不同的比例叠加,就可以产生各种颜色的光,在控制这些颜色比例时,每种颜色的最小值是0,最大值是255,因此理论上这三种基本颜色可以组合成256^3种颜色(16 772 216种),几乎包括了人类视力所能感知的所有颜色。

8.2 计算机是如何显示图形的

数据相片是按照一定的次序使用RGB色彩模型逐个描述每一个像素的颜色。计算机显示器要解决的问题是它要显示的内容是动态的、变化的。目前我们用到图形显示器可以分为:

  • 大型发光二级管显示屏。单色显示屏由单色发光二级管组成,由驱动电路决定每一个二极管的亮或灭。单色显示屏的颜色由发光二级管的颜色决定,人们当然不可能满足于单色显示,怎样才能实现发出各种颜色的光呢?最简单的方式是将三个不同的颜色的发光二极管放到一起并控制它们以不同的亮度发光,它们就可以组合成多种颜色。将三个独立的发光二极管放到一起难免会增加显示屏的体积。多彩发光二级管就是将这三个发光二极管封装到一起以减少每个像素所使用的器件体积。
  • OLED显示屏;如果把发光二级管做得更小、更薄,就可以用于手机和计算机屏幕,一些手机就采用了被称为OLED的显示技术,这种发光二级光被称为有机发光二级管(Organic Ligth-Emitting Diode, OLED),这种发光二级管可以做得很小、很薄。OLED屏幕上每一个像素都由三个发光二级管组成,它们各自的亮度决定了每一个像素的颜色。由于它们足够小,因此只有通过放大镜才能分辨每一个像素。如果一个显示屏长期显示一副画面,则屏幕上的发光二级管老化程度会不一样,这就带来OLED显示屏的硬伤,也就是俗称的“烧屏”。由于OLED显示屏是主动发光的,因此它可以做成透明的。
  • LCD显示屏。LCD全称是液晶显示器(Liquid-Crystal Display, LCD),与OLED不同的是LCD显示屏本身不发光,而是有一个白色的背光光源。LCD显示屏是通过薄膜晶体管控制液晶的偏转角度,进而能控制透过它的背光亮度。透过液晶层的背光再通过彩色滤光片,就变成三种强弱不同的色光,进而组合成种种颜色。

8.3 使用EasyX图形库

在程序中绘制图形,本质上是在和显卡打交道。向显卡中写入数据会改变显示的内容:在显示文本时,将文本的ASCII码值送入显存指定位置就可以了;而在显示图片时只需要将每个像素的RGB颜色值送入指定的显示位置即可,显卡的GPU会根据显示中的内容驱动显示器显示图形。

遗憾的是,现代操作系统基本上都禁止直接操作显卡和访问显卡(这可能导致硬件损坏或操作系统崩溃),绘制图形必须通过操作系统提供的应用程序接口或其他图形库进行!

微软提供了DirectDraw图形库用于绘制图形。但它对于我们来说较为复杂。因此我们选择对初学者更为友好的EasyX图形库。

8.3.1 下载和安装EasyX图形库

直接访问 https://easyx.cn/ 点击右侧的【下载EasyX】即可下载:

Snipaste_2026-05-31_22-28-04.png

双击下载好的安装文件,如EasyX_26.1.1.exe,开始安装:

Snipaste_2026-05-31_22-29-08.png

点击【下一步】,尝试选择我们使用的VS2022:

Snipaste_2026-05-31_22-53-31.png

可以看到,无法选择。

但在官网可以查看:Zip 版本:EasyX 所有版本都支持将安装包的 .exe 扩展名修改为 .7z,直接解压缩需要的库文件。

也就是可以直接将安装文件重命名为.7z文件,然后解压,解压后就可以得到头文件和库文件。

Snipaste_2026-05-31_22-57-05.png

解压后,文件夹中有多个文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
./include/easyx.h
./include/graphics.h
./lib/VC2008/x64/EasyXa.lib
./lib/VC2008/x64/EasyXw.lib
./lib/VC2008/x86/EasyXa.lib
./lib/VC2008/x86/EasyXw.lib
./lib/VC2015/x64/EasyXa.lib
./lib/VC2015/x64/EasyXw.lib
./lib/VC2015/x86/EasyXa.lib
./lib/VC2015/x86/EasyXw.lib
./lib/VC6/x86/EasyXa.lib
./lib/VC6/x86/EasyXw.lib
./readme.txt
./Setup.exe

我们将这4个文件复制到D:\BC101\Libraries\EasyX目录下:

1
2
3
4
./include/easyx.h
./include/graphics.h
./lib/VC2015/x86/EasyXa.lib
./lib/VC2015/x86/EasyXw.lib

然后像之前一样配置附加包含目录,附加库目录、强制输出等信息。

注意,EasyXa.libEasyXw.lib的区别:

  • EasyXa.lib:ANSI 字符集(多字节)

  • EasyXw.lib:Unicode 宽字符(VS 默认项目)不能两个一起链接

8.3.2 初始化图形窗口并绘制一个白色的点

创建新的项目,其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// L08_01_PUT_PIXEL.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/EasyX
// 此处引入自定义的静态库的头文件需要相对路径
#include "graphics.h"

// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/EasyXw.lib":库文件在哪里
// 链接器--附加库目录,加上D:\BC101\Libraries\EasyX 目录
#pragma comment(lib, "EasyXw.lib")

int main()
{
    initgraph(200, 200);
    putpixel(100, 100, 16777215);
    getchar();
    closegraph();

    return 0;
}

运行程序后,可以看到这样一个窗口:

Snipaste_2026-05-31_23-08-45.png

如果不加getchar()这一行代码,程序绘制完图形后,就马上退出了。还没来得及看到图形程序就退出了。

可以看到这个窗口和中心的白点,就表示EasyX图形库已经正确安装并可以正常使用了!

8.4 基本的绘图操作

在进行绘图操作前,先来了解一下屏幕坐标。

什么是屏幕坐标?

简单来说,屏幕坐标是一对数字 (X, Y),用于精确描述显示器上某个位置。每个窗口、按钮、光标和像素都有自己的坐标地址,软件依靠这些地址将内容渲染到正确位置。

这个概念直接来源于数学。在标准笛卡尔平面中,你使用水平距离 (X) 和垂直距离 (Y) 来定位点,起点是 (0, 0) 的原点。计算机借鉴了这个思想,但做了一个重大改变:在屏幕上,Y 轴指向下方而非上方,所以原点在显示器的左上角,而非左下角。

记住:屏幕坐标始终以参考表面的左上角为起点。X 向右增加,Y 向下增加。

Snipaste_2026-07-12_19-28-52.png

详细可参考: 屏幕坐标完全指南:如何获取鼠标位置、像素定位与屏幕中心计算

8.4.1 绘制线

无论是直线、曲线,都可以认为是由“点”组成的,掌握了绘制点的方法就可以绘制所有图形。

8.4.1.1 用多个点来组成直线

既然调用一次putpixel函数可以绘制一个点,如果要绘制多个点,多次调用putpixel函数就可以了!!!

你可以参考书上,多次重复调用putpixel(100, 100, 16777215);函数,并改变x轴坐标。

我为了方便查看,使用for循环,并使用的循环次数比较多。

以下是绘制一个水平线的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// L08_02_DRAW_LINE.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/EasyX
// 此处引入自定义的静态库的头文件需要相对路径
#include "graphics.h"

// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/EasyXw.lib":库文件在哪里
// 链接器--附加库目录,加上D:\BC101\Libraries\EasyX 目录
#pragma comment(lib, "EasyXw.lib")

int main()
{
    initgraph(200, 200);
    for (int x = 50; x <= 100; x++) {
        putpixel(x, 100, 16777215);
    }

    getchar();
    closegraph();

    return 0;
}

运行程序输出如下图所示:

Snipaste_2026-07-12_19-43-19.png

同样,如果要绘制一个垂直线,只用控制x轴不变,y轴改变就行。以下是绘制的垂直线。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// L08_02_DRAW_LINE.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/EasyX
// 此处引入自定义的静态库的头文件需要相对路径
#include "graphics.h"

// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/EasyXw.lib":库文件在哪里
// 链接器--附加库目录,加上D:\BC101\Libraries\EasyX 目录
#pragma comment(lib, "EasyXw.lib")

int main()
{
    initgraph(200, 200);
    /*for (int x = 50; x <= 100; x++) {
        putpixel(x, 100, 16777215);
    }*/

    for (int y = 50; y <= 100; y++) {
        putpixel(50, y, 16777215);
    }

    getchar();
    closegraph();

    return 0;
}

运行程序输出如下图所示:

Snipaste_2026-07-12_19-50-03.png

8.4.1.2 绘制水平线的函数

对于水平线来说,组成它的像素点y轴坐标不变,x轴坐标变化。因此,在绘制水平线时需要通过参数指定3个条件:

  • x轴起始点坐标x1;
  • x轴终止点坐标x2;必须大于或等于x1;
  • y轴会标。

绘制水平线的函数的名称可以是drawHorizontalLine,函数可以不需要返回值,因此函数的原型是:

1
void drawHorizontalLine(int x1, int x2, int y);

函数化后,再来绘制水平线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// L08_02_DRAW_LINE.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/EasyX
// 此处引入自定义的静态库的头文件需要相对路径
#include "graphics.h"

// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/EasyXw.lib":库文件在哪里
// 链接器--附加库目录,加上D:\BC101\Libraries\EasyX 目录
#pragma comment(lib, "EasyXw.lib")

// 绘制水平线
void drawHorizontalLine(int x1, int x2, int y);

int main()
{
    initgraph(200, 200);
    drawHorizontalLine(50, 100, 100);

    getchar();
    closegraph();

    return 0;
}

void drawHorizontalLine(int x1, int x2, int y)
{
    if (x2 < x1) {
        int temp = x1;
        x1 = x2;
        x2 = temp;
    }
    for (int x = x1; x <= x2; x++) {
        putpixel(x, y, 16777215);
    }
}

此时,可以看到,与之前的水平线输出是一样的!

8.4.1.3 绘制垂直线的函数

对于垂直线来说,组成它的像素点x轴坐标不变,y轴坐标变化。因此,在绘制垂直线时需要通过参数指定3个条件:

  • y轴起始点坐标y1;
  • y轴终止点坐标y2;必须大于或等于y1;
  • x轴会标。

绘制垂直线的函数的名称可以是drawVerticalLine,函数可以不需要返回值,因此函数的原型是:

1
void drawVerticalLine(int y1, int y2, int x);

函数化后,再来绘制垂直线,在上一节的基础上,再加上绘制垂直线的函数,并绘制垂直线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// L08_02_DRAW_LINE.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/EasyX
// 此处引入自定义的静态库的头文件需要相对路径
#include "graphics.h"

// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/EasyXw.lib":库文件在哪里
// 链接器--附加库目录,加上D:\BC101\Libraries\EasyX 目录
#pragma comment(lib, "EasyXw.lib")

// 绘制水平线
void drawHorizontalLine(int x1, int x2, int y);
// 绘制垂直线
void drawVerticalLine(int y1, int y2, int x);

int main()
{
    initgraph(200, 200);
    drawHorizontalLine(50, 100, 100);
    drawVerticalLine(50, 100, 50);

    getchar();
    closegraph();

    return 0;
}

void drawHorizontalLine(int x1, int x2, int y)
{
    if (x2 < x1) {
        int temp = x1;
        x1 = x2;
        x2 = temp;
    }
    for (int x = x1; x <= x2; x++) {
        putpixel(x, y, 16777215);
    }
}

void drawVerticalLine(int y1, int y2, int x)
{
    if (y2 < y1) {
        int temp = y1;
        y1 = y2;
        y2 = temp;
    }
    for (int y = y1; y <= y2; y++) {
        putpixel(x, y, 16777215);
    }
}

运行程序输出如下图所示:

Snipaste_2026-07-12_20-30-46.png

Licensed under the GNU General Public License v3.0
最后更新于 2026年07月12日 20:31