mei

一次函数图象生成

编写原因

最近学习了初二数学的一次函数,于是突发奇想,写了这个程序。

编写过程

主要的难点只有 2 个(可能是对我而言):

1、判断表达式的合法性。

2、绘制平面直角坐标系。

3、绘制函数图象。

其他一些界面,背景之类的都较简单。

判断表达式的合法性

输入字符串后,将其转为 string 类型,再利用正则表达式进行检测并匹配出结果。最后将结果转换为数字赋值入 k 和 b。

	char t[100] = {}, a1[50] = {}, a2[50] = {};
	LPCTSTR title = "表达式输入";
	
	settextcolor(BLACK);
	
	Input_yn = InputBox(t, 100, "请输入形如 y = k x + b 形式的一次函数表达式:", title, NULL, 0, 0, false);
	int len = strlen(t);

	// 目标串
	string s = t;

	// 模式串
	regex reg("y\\s*=\\s*([+-]?)\\s*(\\d*(\\.\\d*)?)?\\s*x\\s*(([+-])\\s*(\\d*(\\.\\d*)?))?");

	// 结果集
	smatch result;

	// 匹配
	if (regex_search(s, result, reg))					// 匹配一个结果
	{
		string sk1 = result[1], sk2 = result[2],		// result[1] 是符号
			sb1 = result[4], sb2 = result[5];			// result[4] 是符号
		sk1 += sk2;
		sb1 += sb2;
		bool isassk = false, isassb = false;			// 记录是否转换 k b
		if (sk1.empty())
		{
			k = 1;
			isassk = true;
		}
		if (sb1.empty())
		{
			b = 0;
			isassb = true;
		}
		if (!isassk)
		{
			k = stod(sk1, 0);
			if (k == 0)
				return false;
		}
		if (!isassb)
			b = stod(sb1, 0);
	}

	else
		return false;

	return true;

因为 k 值不可为 0,所以判断若 k 为 0,重新输入。

绘制平面直角坐标系

主要难点在于标记数字和绘制刻度。

因为每一个刻度之间距离相等,所以可以想到用循环进行绘制。

既然绘制刻度如此,那么标记数字也是这样。定义一个整型,循环中不断加上单位长度(一般为 1),再转换为字符串进行输出。想要数字与刻度对齐,就要求其字符串高度及宽度。

以下是绘制 x 轴刻度以及标记数字的代码,d 为常量 40(代表一单位长度实际长 40 像素),sum_x 初始值(为负)由常量 width(窗口宽)决定,逐次加 1 转换为字符串以输出,height 是窗口高,y 轴同理。

for (int i = 20; i <= width - 20; i += d)
{
	sum_x++;
	if (sum_x == 0) continue;
	line(i, height / 2, i, height / 2 - 10);
	char j[100];
	sprintf_s(j, "%d", sum_x);
	outtextxy(i - textwidth(j) / 2, height / 2 + 5, j);
}

绘制函数图象

i 即 x 轴上的“点”,用 numx 来标记。

for (double i = 1; i <= 1001; i++)
{
	if (fabs(k) * i < (width - 40) / d / 2)
		continue;
	else numx = i;
}

因为 numx 为标记的 x 轴坐标,所以 d * numx + 或 - width / 2 的绝对值便是图象一端 x 坐标。因为 y = k x + b, 所以图象一端的 y 坐标即 height / 2 + 或 - numx * fabs(k) * d + 或 - b * d。

以下是函数图象绘制代码,determine() 函数即上文对 numx 赋值的代码。

	if (b)								// 判断 b 是否为零
	{
		// b 非零
		if (k > 0)						// 判断 k 的正负性
		{
			// k 为正
			determine();				// 确定坐标
			if (b > 0) line(numx * d + width / 2, height / 2 - numx * k * d - b * d,
							width / 2 - numx * d, height / 2 + numx * k * d - b * d);

			else line(numx * d + width / 2, height / 2 - numx * k * d + fabs(b) * d,
						width / 2 - numx * d, height / 2 + numx * k * d + fabs(b) * d);
		}

		else
		{
			// k 为负
			determine();				// 确定坐标
			if (b > 0)line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d - b * d,
							width / 2 + numx * d, height / 2 + numx * fabs(k) * d - b * d);

			else line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d + fabs(b) * d,
						width / 2 + numx * d, height / 2 + numx * fabs(k) * d + fabs(b) * d);
		}
	}

	else
	{
		// b 为零
		if (k > 0)						// 判断 k 的正负性
		{
			// k 为正
			determine();				// 确定坐标
			line(numx * d + width / 2, height / 2 - numx * k * d,
				width / 2 - numx * d, height / 2 + numx * k * d);
		}

		else
		{
			// k 为负
			determine();				// 确定坐标
			line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d,
				width / 2 + numx * d, height / 2 + numx * fabs(k) * d);
		}
	}

同时,为了同时绘制多个函数图象,可以通过保留上一次的函数图象后不断叠加绘制新的图象并随机图象颜色来形成这种效果。

源码:

///////////////////////////////////////////////////////////////
// 程序名称:一次函数生成器
// 编译环境:Mictosoft Visual Studio 2022 | EasyX_20220610
// 作	 者:繁星若尘 <2893820887@qq.com>
// 学	 校:合肥市行知中学
// 最后修改:2022-9-1
//

#undef UNICODE
#undef _UNICODE

#include <graphics.h>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <ctime>
#include <regex>

using namespace std;

IMAGE img, img1;										// 保存图片使用
double const d = 40;									// 单位长度
double const width = 680;								// 窗口宽
double const height = 680;								// 窗口高
int sum_Image;
double k = 0;											// x 的系数 k
double b = 0;											// b
double numx = 0;										// 帮助记录 x 坐标


// 绘制按钮
void DrawKey()
{
	char a[15] = "添加函数";
	char b[15] = "清空图象";
	settextstyle(20, 0, "微软雅黑", 0, 0, 0, false, false, false);
	setfillcolor(WHITE);
	settextcolor(BLACK);

	int const w = textwidth(a);
	int const h = textheight(a);

	solidrectangle(width - 20 - w, 0, width, h + 20);
	solidrectangle(width - 20 - w, h + 20, width, 2 * h + 40);
	outtextxy(width - 10 - w, 10, a);
	outtextxy(width - 10 - w, h + 30, b);
}

// 绘制平面直角坐标系
void DrawCoordinate()
{	
	setbkcolor(WHITE);
	settextcolor(BLACK);
	cleardevice();
	settextstyle(20, 0, "Times New Roman");
	outtextxy(0, 0, "函数表达式:");
	DrawKey();
	setlinecolor(BLACK);
	setbkmode(TRANSPARENT);								// 字体背景透明
	outtextxy(width - 20, height / 2 - 26, 'x');
	outtextxy(width / 2 + 18, 0, 'y');
	int sum_x = -(width - 40) / d / 2 - 1;				// 用来标长度
	int sum_y = -(height - 40) / d / 2 - 1;

	line(0, height / 2, width, height / 2);				// 绘制 x , y 轴
	line(width / 2, 0, width / 2, height);
	line(width / 2, 0, width / 2 + 10, 10);
	line(width / 2, 0, width / 2 - 10, 10);
	line(width, height / 2, width - 10, height / 2 + 10);
	line(width, height / 2, width - 10, height / 2 - 10);

	for (int i = 20; i <= width - 20; i += d)			// 绘制 x 轴刻度以及标记数字
	{
		sum_x++;
		if (sum_x == 0) continue;
		line(i, height / 2, i, height / 2 - 10);
		char j[100];
		sprintf_s(j, "%d", sum_x);
		outtextxy(i - textwidth(j) / 2, height / 2 + 5, j);
	}

	for (int i = height - 20; i >= 20; i -= d)			// 绘制 y 轴刻度以及标记数字
	{
		sum_y++;
		if (sum_y == 0) continue;
		line(width / 2, i, width / 2 + 10, i);
		char j[100];
		sprintf_s(j, "%d", sum_y);
		outtextxy(width / 2 - 10 - textwidth(j) / 2, i - textheight(j) / 2, j);
	}
	
	char name[] = "一次函数生成器";
	outtextxy(width - textwidth(name), height - textheight(name), name);

	settextstyle(20, 0, "Times New Roman", 300, 300, 10, true, false, false);

	outtextxy(width / 2 - 22, height / 2 - 18, 'O');	// 标记原点
	getimage(&img1, 0, 0, width, height);
}

// 确定坐标
void determine()
{
	for (double i = 1; i <= 1001; i++)				// 为了在 k 十分小时仍可判断
	{
		if (fabs(k) * i < (width - 40) / d / 2)		// 判断 k 的绝对值与 i 相乘是否小于 x 半轴数字最大值
			continue;
		else numx = i ;								// i 就相当于 x 轴上的数,判断 k 的绝对值与 i 相乘是否小于 x 半轴数字最大值
													// 是为了避免函数图象过短
	}
}

// 绘制函数图象同时写出函数表达式
void DrawImage()
{
	srand((int)time(NULL));

	settextstyle(20, 0, "Times New Roman", 0, 0, 0, false, false, false);

	int H = rand() % 341;															// 随机图象和文字颜色
	setlinecolor(HSLtoRGB(H, 1, 0.3));
	settextcolor(HSLtoRGB(H, 1, 0.3));
	putimage(0, 0, &img);

	
	char s2[50] = "y = ", s3[15] = {}, s4[15] = {}, s5[15] = " x ", s6[15] = "+ ";	// 定义多个字符数组逐个拼接
	if (k != 1 && k > 0 || k < 0)													// 将 k 值转为字符串
		sprintf_s(s3, "%g", k);
	if (k == -1)																	// 当 k 为负 1 只将负号转为字符串
	{
		memset(s3, 0, sizeof(s3));													// 清空字符串
		s3[0] = '-';
	}
	strcat_s(s2, s3);

	bool B_0_yn = false;															// 记录 b 值是否为 0

	if (b < 0)
	{
		b = fabs(b);
		B_0_yn = true;
	}

	if (b != 0)
		sprintf_s(s4, "%g", b);
	if (B_0_yn)
	{
		s6[0] = '-';
		s6[1] = ' ';
		if (b) sprintf_s(s2, "%s%s%s%s", s2, s5, s6, s4);
		else sprintf_s(s2, "%s%s", s2, s5);
		b = -b;
	}

	else
	{
		if (b)
			sprintf_s(s2, "%s%s%s%s", s2, s5, s6, s4);
		else
			sprintf_s(s2, "%s%s", s2, s5);
	}

	// 输出函数表达式
	outtextxy(0, textheight("函数表达式:") + sum_Image * textheight(s2), s2);

	if (b)								// 判断 b 是否为零
	{
		// b 非零
		if (k > 0)						// 判断 k 的正负性
		{
			// k 为正
			determine();				// 确定坐标
			if (b > 0) line(numx * d + width / 2, height / 2 - numx * k * d - b * d,
							width / 2 - numx * d, height / 2 + numx * k * d - b * d);

			else line(numx * d + width / 2, height / 2 - numx * k * d + fabs(b) * d,
						width / 2 - numx * d, height / 2 + numx * k * d + fabs(b) * d);
		}

		else
		{
			// k 为负
			determine();				// 确定坐标
			if (b > 0)line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d - b * d,
							width / 2 + numx * d, height / 2 + numx * fabs(k) * d - b * d);

			else line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d + fabs(b) * d,
						width / 2 + numx * d, height / 2 + numx * fabs(k) * d + fabs(b) * d);
		}
	}

	else
	{
		// b 为零
		if (k > 0)						// 判断 k 的正负性
		{
			// k 为正
			determine();				// 确定坐标
			line(numx * d + width / 2, height / 2 - numx * k * d,
				width / 2 - numx * d, height / 2 + numx * k * d);
		}

		else
		{
			// k 为负
			determine();				// 确定坐标
			line(width / 2 - numx * d, height / 2 - numx * fabs(k) * d,
				width / 2 + numx * d, height / 2 + numx * fabs(k) * d);
		}
	}

	getimage(&img, 0, 0, width, height);
	sum_Image++;
}

bool Input_yn = false;														// 记录是否输入
// 输入函数表达式
bool input()
{
	k = 0, b = 0;
	Input_yn = false;
	char t[100] = {}, a1[50] = {}, a2[50] = {};
	LPCTSTR title = "表达式输入";
	
	settextcolor(BLACK);
	
	Input_yn = InputBox(t, 100, "请输入形如 y = k x + b 形式的一次函数表达式:", title, NULL, 0, 0, false);
	int len = strlen(t);

	// 目标串
	string s = t;

	// 模式串
	regex reg("y\\s*=\\s*([+-]?)\\s*(\\d*(\\.\\d*)?)?\\s*x\\s*(([+-])\\s*(\\d*(\\.\\d*)?))?");

	// 结果集
	smatch result;

	// 匹配
	if (regex_search(s, result, reg))					// 匹配一个结果
	{
		string sk1 = result[1], sk2 = result[2],		// result[1] 是符号
			sb1 = result[4], sb2 = result[5];			// result[4] 是符号
		sk1 += sk2;
		sb1 += sb2;
		bool isassk = false, isassb = false;			// 记录是否转换 k b
		if (sk1.empty())
		{
			k = 1;
			isassk = true;
		}
		if (sb1.empty())
		{
			b = 0;
			isassb = true;
		}
		if (!isassk)
		{
			k = stod(sk1, 0);
			if (k == 0)
				return false;
		}
		if (!isassb)
			b = stod(sb1, 0);
	}

	else
		return false;

	return true;
}

// 是否继续绘制函数图象
bool reDrawImage_yn()
{
	ExMessage m;
	DrawKey();																			// 绘制按钮
	char a[15] = "添加函数";
	char b[15] = "清空图象";
	int const w = textwidth(a);
	int const h = textheight(a);
	static bool isBtn1Hover = false, isBtn2Hover = false;								// 记录鼠标是否悬停在按钮上

	while(true)
	{
		m = getmessage(EM_MOUSE);														// 获取鼠标信息
		bool button1 = (m.x >= width - 20 - w && m.y >= 0 && m.x <= width && m.y <= h + 20),
			button2 = (m.x >= width - 20 - w && m.y >= h + 20 && m.x <= width && m.y <= 2 * h + 40);
		if (button1)																	// 当鼠标放在“添加函数”按钮(即按钮 1)上时
		{
			if (!isBtn1Hover)
			{
				setfillcolor(RGB(50, 50, 255));											// 对按钮底色和前景色进行变色操作
				settextcolor(WHITE);
				solidrectangle(width - 20 - w, 0, width, h + 20);
				outtextxy(width - 10 - w, 10, a);
			}
			isBtn1Hover = true;
			if (m.message == WM_LBUTTONDOWN) return true;								// 检测鼠标左键是否按在指定位置
		}
		else
		{
			if (isBtn1Hover)
			{
				setfillcolor(WHITE);													// 当鼠标不放在按钮上面时,变回原样
				settextcolor(BLACK);
				solidrectangle(width - 20 - w, 0, width, h + 20);
				outtextxy(width - 10 - w, 10, a);
			}
			isBtn1Hover = false;
		}
		if (button2)																	// 当鼠标放在“清空图象”按钮(即按钮 2)上时
		{
			if (!isBtn2Hover)
			{
				setfillcolor(RGB(50, 50, 255));											// 对按钮底色和前景色进行变色操作
				settextcolor(WHITE);
				solidrectangle(width - 20 - w, h + 20, width, 2 * h + 40);
				outtextxy(width - 10 - w, h + 30, b);
			}
			isBtn2Hover = true;
			if (m.message == WM_LBUTTONDOWN) return false;								// 检测鼠标左键是否按在指定位置
		}
		else
		{
			if(isBtn2Hover)
			{
				setfillcolor(WHITE);													// 当鼠标不放在按钮上面时,变回原样
				settextcolor(BLACK);
				solidrectangle(width - 20 - w, h + 20, width, 2 * h + 40);
				outtextxy(width - 10 - w, h + 30, b);
			}
			isBtn2Hover = false;
		}
	}
}

void continue_main()
{
	cleardevice();
	settextstyle(20, 10, "微软雅黑");
	DrawCoordinate();				// 绘制平面直角坐标系
	while (true)
	{
		if (reDrawImage_yn())		// 是否继续绘制函数图象
		{
			if (input())
			{
				if(Input_yn)
					DrawImage();
			}

			else
			{
				HWND wnd = GetHWnd();
				MessageBox(wnd, "输入错误", "错误", MB_ICONEXCLAMATION);
			}
		}

		else
		{
			// 将变量回归初始状态
			k = 0;
			b = 0;
			img = img1;
			sum_Image = 0;
			continue_main();
		}
	}
}

int main()
{
	initgraph(width, height);			// 画布
	LOGFONT font;
	gettextstyle(&font);				// 抗锯齿绘图
	font.lfQuality = DEFAULT_QUALITY;
	settextstyle(&font);
	continue_main();
	closegraph();						// 关闭画布
}

运行后,可能会出现大量的 double 转 int 警告,不过经过我多次测试后发现,这并没有什么影响。

效果如下( y = -0.123 x + 1, y = 2.3456 x - 1.345, y = 0.618 x + 2):

评论 (2) -

  • 读取的时候直接sscanf(t, "y = %lf x + %lf", &k, &b);就行了,要是想判断输入正确不正确,sscanf的返回值是读取的数量,if (sscanf(t, "y = %lf x + %lf", &k, &b) != 2)return false;就行了,不用那么麻烦。
    • 你这样无法识别“y=2x"以及”y=2x-5"这种情况,也会错误的通过"y=2x+5+6"。

添加评论