纯C语言原生实现可视化彩色按钮效果

目录

〇、引子

一、如何实现彩色输出?

二、如何将光标放置在指定位置 ?

三、如何确定鼠标点击按钮?

四、封装按钮

五、附加功能:选中高亮,和颜色宏定义

六、后记


〇、引子

你还在通过“请输入数字选择下一步操作”来做用户交互界面吗?


C语言中,纯文字的交互界面一直是新人学习过程中的一种缺憾。在习惯使用现代丰富多彩的图形交互界面的情况下,看着自己写出来的黑框框,总是有一种无力感。

为了解决这种缺憾,本文希望通过C语言原生的函数语法,搭配windows API,来实现一种可以显示在任意位置,标注任意文字,拥有任意颜色的,可以点击的按钮。

让我们抽丝剥茧地思考问题:要实现一个拥有如上功能的按钮,需要逐步实现哪些效果?

.

.

.

.

.

.

想出来了吗? 

“先将光标放在指定位置,然后改变输出颜色,显示字符,最后再检测鼠标是否在按钮上按下”

也就是:

1.彩色输出(为了显示彩色文字)

2.移动光标(为了将按钮显示到指定位置)

3.检测鼠标事件(比如鼠标位置、是否点击等等)

为了不吊大家的胃口,在这里先把实现代码放出来。

如果不出意外的话,这段代码可以在你的编译器里直接运行(至少在gcc和clang下

#include <stdio.h>
#include <string.h>
#include <Windows.h>

#define BLUE (FOREGROUND_INTENSITY | FOREGROUND_BLUE)
#define GREEN (FOREGROUND_INTENSITY | FOREGROUND_GREEN)
#define RED (FOREGROUND_INTENSITY | FOREGROUND_RED)
#define CYAN (FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_GREEN)
#define PURPLE (FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_RED)
#define YELLOW (FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED)
#define WHITEBGBLACKFONT ((BACKGROUND_INTENSITY | (BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED)) | (FOREGROUND_INTENSITY | !(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)))
#define DEFAULT (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)
#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)  //用来检测按键的点击事件

typedef struct mouse {	//鼠标型变量,包含两个int型变量,表示坐标
	int x;
	int y;
} mouse;
mouse clickk,not_click_yet;			//全局变量,表示鼠标上一次按下的位置,和当前位置

void SetWordAt(int x, int y);								//实现将输入光标放置到任意位置			
mouse WhereIClick();										//实现返回当前鼠标相对窗口的位置
int ColorfulPrintf(WORD color, const char* format, ...);	//实现输出彩色字符,参数格式为[颜色][字符串]{[变量]}...即在传统的printf中加入了颜色参数
int DrawButton(int sum, ...);								//绘制任意个任意颜色,任意位置的按钮,当按下时返回按钮编号。参数格式为[总按钮个数]{[按钮文字][按钮颜色][按钮位置]}...
mouse WhereIWillClick();									//检测鼠标当前的位置

int ColorfulPrintf(WORD color, const char* format, ...) {
	char str[256];			// 要输出的字符串
	va_list ap;				// 指向参数的指针
	va_start(ap, format);	// 初始化指向参数的指针
	HANDLE consolehwnd = GetStdHandle(STD_OUTPUT_HANDLE);	// 输出窗口的句柄
	SetConsoleTextAttribute(consolehwnd, color);			// 改为使用预期的颜色输出
	int ret = vsprintf(str, format, ap);					//返回值为变量数
	printf("%s", str);										//输出加工好(塞入变量)的字符串
	SetConsoleTextAttribute(consolehwnd, DEFAULT);			// 恢复默认效果
	va_end(ap);				// 结束可变参数的获取
	return ret;				//返回变量数
}

void SetWordAt(int x, int y) {
	HANDLE screen = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD position;			// position 是一个 COORD 结构
	position.X = x;			//设置列
	position.Y = y;			//设置行
	SetConsoleCursorPosition(screen, position);//将输入光标移动在那里
}

mouse WhereIClick() {
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	HWND h = GetForegroundWindow();
	CONSOLE_FONT_INFO consoleCurrentFont;
	
	mouse click;
	//----------移除快速编辑模式(对于win10用户)----------
	HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
	DWORD mode;
	GetConsoleMode(hStdin, &mode);
	mode &= ~ENABLE_QUICK_EDIT_MODE;
	SetConsoleMode(hStdin, mode);

	//----------检测----------
	if (KEY_DOWN(VK_LBUTTON)) {  			//鼠标左键按下
		POINT p;
		GetCursorPos(&p);
		ScreenToClient(h, &p);              //获取鼠标在窗口上的位置
		GetCurrentConsoleFont(hOutput, FALSE, &consoleCurrentFont); 	//获取字体信息
		click.x = p.x /= consoleCurrentFont.dwFontSize.X;				//矫正x坐标为字符尺度
		click.y = p.y /= consoleCurrentFont.dwFontSize.Y;				//矫正x坐标为字符尺度
	}
	return click;
}

mouse WhereIWillClick() {
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	HWND h = GetForegroundWindow();
	CONSOLE_FONT_INFO consoleCurrentFont;
	
	mouse click;
	//----------移除快速编辑模式(对于win10用户)----------
	HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
	DWORD mode;
	GetConsoleMode(hStdin, &mode);
	mode &= ~ENABLE_QUICK_EDIT_MODE;
	SetConsoleMode(hStdin, mode);
	
	//----------检测----------
		POINT p;
		GetCursorPos(&p);
		ScreenToClient(h, &p);              //获取鼠标在窗口上的位置
		GetCurrentConsoleFont(hOutput, FALSE, &consoleCurrentFont); 	//获取字体信息
		click.x = p.x /= consoleCurrentFont.dwFontSize.X;				//矫正x坐标为字符尺度
		click.y = p.y /= consoleCurrentFont.dwFontSize.Y;				//矫正x坐标为字符尺度

	return click;
}

int DrawButton(int sum, ...) {	
	struct {					//建立按钮结构体
		char *line;
		WORD color;
		mouse location;
	} buttons[sum];
	
	va_list valist;
	va_start(valist, sum);								//指定可变参数起点
	for (int i = 0; i < sum; i++) {						//循环读取每一组按钮的数据
		buttons[i].line = va_arg( valist, char *);		//按钮文字
		buttons[i].color = va_arg( valist, int);		//按钮颜色
		buttons[i].location = va_arg( valist, mouse);	//按钮位置
	}
	va_end(valist);	
	
	for (int i = 0; i < sum; i++) {						//在指定位置显示第i个按钮
		SetWordAt(buttons[i].location.x, buttons[i].location.y);
		ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
	}
	int sign[sum]={0};//为了选中/取消选中时不闪
	while (1) {
		clickk = WhereIClick();							//当鼠标左键每按下一次,记录这时的坐标
		not_click_yet=WhereIWillClick();				//记录本循环里鼠标的坐标,用来高亮按钮
		for (int i = 0; i < sum; i++) {					//循环检测所有按钮
			if (sign[i]==0&&not_click_yet.y == buttons[i].location.y && not_click_yet.x > buttons[i].location.x && not_click_yet.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被选中时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				//将第i个按钮高亮显示
				SetWordAt(buttons[i].location.x, buttons[i].location.y);
				ColorfulPrintf(WHITEBGBLACKFONT, "%s", buttons[i].line);
				sign[i]=1;
			} 
			if(sign[i]==1&&!(not_click_yet.y == buttons[i].location.y && not_click_yet.x > buttons[i].location.x && not_click_yet.x < (int)(buttons[i].location.x + strlen(buttons[i].line)))) {
				//如果没有被选中,取消高亮
				SetWordAt(buttons[i].location.x, buttons[i].location.y);
				ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
				sign[i]=0;
			}

			if (clickk.y == buttons[i].location.y && clickk.x > buttons[i].location.x && clickk.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被按下时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				system("cls");		//清屏
				return i;			//返回i,即被按下的按钮的编号
			} 
		}
		Sleep(50);	//检测鼠标按下的间隔50ms
	}
}

int main() {
	mouse loc[5];
	for(int i=0;i<5;i++){
		loc[i].x=30;
		loc[i].y=i*2+4;
	}

	return DrawButton(4, "一个按钮示例", RED, loc[0], "两个按钮示例", YELLOW, loc[1], "三个按钮示例", BLUE, loc[2], "四个按钮示例", GREEN, loc[3]);
}

看起来很繁琐是吗?不要紧,我现在再回看这些代码也是这种感觉(*-*)不过只要你跟上本文的步伐,就一定可以搞明白这团东西是什么

现在,我们边看代码,边进入主题,抽丝剥茧地分析一下这团东西是如何实现功能的。


一、如何实现彩色输出?

俗话说,不积小流,无以成江海 ;百年树木,发于春芽。要实现一个完整的按钮输出,我们也要先从小处着手。

在Windows API中,有一个很棒的函数,叫做 SetConsoleTextAttribute 。它可以帮助我们显示彩色字符。关于这个函数的详细介绍,可以参考以下这篇文章,但是建议先不急着看,先跟着本文的进度走

《SetConsoleTextAttribute函数用法》

        很多时候,我们希望编写出能够拥有各种颜色的程序,能够让人眼前一亮,虽然system(color)函数可以设置颜色,但是system(color)函数设置的颜色只能有一种,这显然不是我们想要的结果,我们需要的是一个名为SetConsoleTextAttribute的函数。
        SetConsoleTextAttribute()函数是一个API设置字体颜色和背景色的函数。参数表中使用两个属性(属性之间用,隔开),不同于system(color),SetConsoleTextAttribute()可以改变界面多种颜色,而system()只能修改为一种!......

 具体的实现方式我们不用管他,只要会用就好。SetConsoleTextAttribute函数需要两个参数,分别是当前窗口的句柄需要输出的颜色

什么?你还不知道句柄是什么意思?其实我们并不需要知道句柄是什么东西,这不影响我们继续。我们可以简单地把他理解成“窗口的代号”。如果想让这个函数改变当前窗口的颜色为黑底红字,我们只需要这样写:

HANDLE consolehwnd = GetStdHandle(STD_OUTPUT_HANDLE);	// 获取输出窗口的句柄
SetConsoleTextAttribute(consolehwnd, 0x0C);			// 改为使用黑底红字输出

其中consolehwnd就是所谓的句柄,而0x0c代表黑底红字,是16进制写法。具体的数字-颜色对应关系可以参照下表:

在这里插入图片描述

 当然,目前我们只是改变了当前窗口的输出模式,但是还没有输出。如果在上面两句后面加一句

printf("猜猜我什么颜色?");

就会发现输出的字符已经变色了!

当然,在输出结束后,记得用

SetConsoleTextAttribute(consolehwnd, DEFAULT);			// 恢复默认效果

 来将输出颜色恢复默认,要不然之后你输出任何字都是红色咯!

到这里,我们已经完成了彩色输出的任务。那么我们能不能把这几句封装到一个函数里面,就像这样?

int ColorfulPrintf(WORD color, const char* format, ...);

int a=2,b=3;
ColorfulPrintf(0x0c, "a = %d , b = %d .", a , b );

可能或许说不定你会看不懂第一句这个函数定义,因为它用到了C语言的可变参数这一知识点。关于可变参数,可以参考菜鸟教程的这篇教程

没关系,和上面的什么句柄一样,我们在使用时并不需要非常关心这些,只需要知道这是为了实现像printf函数一样可以输出变量,就行了。

让我们看一下打包好的函数长什么样:

int ColorfulPrintf(WORD color, const char* format, ...) {
	char str[256];			// 要输出的字符串
	va_list ap;				// 指向参数的指针
	va_start(ap, format);	// 初始化指向参数的指针
	HANDLE consolehwnd = GetStdHandle(STD_OUTPUT_HANDLE);	// 输出窗口的句柄
	SetConsoleTextAttribute(consolehwnd, color);			// 改为使用预期的颜色输出
	int ret = vsprintf(str, format, ap);					//返回值为变量数
	printf("%s", str);										//输出加工好(塞入变量)的字符串
	SetConsoleTextAttribute(consolehwnd, DEFAULT);			// 恢复默认效果
	va_end(ap);				// 结束可变参数的获取
	return ret;				//返回变量数
}

恭喜你!现在我们已经可以随心所欲的彩色输出了!


二、如何将光标放置在指定位置 ?

这个就更简单了!只需要调用一个函数即可,也不需要什么可变参数。

SetConsoleCursorPosition(screen, position);//将输入光标移动在那里

其中,第一个参数也是句柄,第二个参数是一个结构体,包含一个x和一个y,分别代表输入光标要移动到的位置的x和y坐标。这个函数的意思是,把输入光标挪动到screen句柄指向的窗口的坐标为position的地方。

关于如何获取句柄,我们上面已经提过了,就是用 GetStdHandle 函数。

HANDLE screen = GetStdHandle(STD_OUTPUT_HANDLE);

 所以,我们可以这样写来试验一下:

    HANDLE screen = GetStdHandle(STD_OUTPUT_HANDLE);//获取当前窗口句柄
	COORD position;
	position.X = 5;			//设置第五列
	position.Y = 5;			//设置第五行
	SetConsoleCursorPosition(screen, position);//将输入光标移动在那里
    printf("猜猜我的位置?");

如果不出意外的话,程序会在第五行第五列输出一句“猜猜我的位置?”

如果和上面的彩色输出函数结合起来的话,就可以把任意彩色字符输出到任意位置了!

同样的,我们把这个函数也打包起来,方便使用:

void SetWordAt(int x, int y) {
	HANDLE screen = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD position;			// position 是一个 COORD 结构
	position.X = x;			//设置列
	position.Y = y;			//设置行
	SetConsoleCursorPosition(screen, position);//将输入光标移动在那里
}

这个函数可以将输入光标放置在第y行第x列。试试吗?

现在我们已经实现了在任意位置显示一个按钮了,只不过没法点击。接下来,最后一个,也是最重要的一个功能,要来了!


三、如何确定鼠标点击按钮?

在前面两节,我们做到了可以通过

SetWordAt(5, 5);
ColorfulPrintf(0x0c,"一个红色按钮");

来实现在第五行第五列绘制一个红色按钮了。那么,如何实现检测按钮被点击呢?

在Windows API中  (又提到它了,就在这里简单解释一下吧。Windows API在C语言里面的具象就是<windows.h>。它是windows系统专用的头文件,可以实现一些诸如键盘监测,鼠标检测,窗口检测,模拟鼠标或键盘动作等等效果。对于Windows API的具体解释说明可以参照.....emm,还是直接百度吧),有一个函数叫做

GetAsyncKeyState(int vKey);

 它的作用是检测当前按下的按键注意,这个函数检测的是物理按键,键盘映射什么的对它无效哦

一般,我们会这样定义一个宏来进行按键检测:

#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)  
//用来检测按键的点击事件

 可能你会疑惑,获取了点击事件之后为什么还要 & 0x8000 呢?其实我也不知道

这个问题的答案与 GetAsyncKeyState 函数的返回值有关,如果展开的话就把问题复杂化了。还是那句话,不影响使用,这么写就完事了。简单来说就是

获取按键状态,屏蔽掉其他的可能状态,按照MSDN上说低位should ignore

 宏定义里面的VK_NONAME与实际的按键对照表如下:

//来源:http://www.manongjc.com/article/51455.html

VK_LBUTTON             鼠标左键                      0x01
VK_RBUTTON             鼠标右键                      0x02
VK_CANCEL              Ctrl + Break                  0x03
VK_MBUTTON             鼠标中键                      0x04

VK_BACK                Backspace 键       0x08
VK_TAB                 Tab 键                        0x09

VK_RETURN              回车键                        0x0D


VK_SHIFT               Shift 键                      0x10
VK_CONTROL             Ctrl 键                       0x11
VK_MENU                Alt 键                 0x12
VK_PAUSE               Pause 键                      0x13
VK_CAPITAL             Caps Lock 键                  0x14

VK_ESCAPE              Esc 键                        0x1B

VK_SPACE               空格键         0x20
VK_PRIOR               Page Up 键                    0x21
VK_NEXT                Page Down 键                  0x22
VK_END                 End 键                        0x23
VK_HOME                Home 键                       0x24
VK_LEFT                左箭头键                      0x25
VK_UP                  上箭头键                      0x26
VK_RIGHT               右箭头键                      0x27
VK_DOWN                下箭头键                      0x28
VK_SNAPSHOT            Print Screen 键               0x2C
VK_Insert              Insert 键                     0x2D
VK_Delete              Delete 键                     0x2E

'0' – '9'             数字 0 - 9                    0x30 - 0x39
'A' – 'Z'             字母 A - Z                    0x41 - 0x5A

VK_LWIN                左WinKey(104键盘才有)         0x5B
VK_RWIN                右WinKey(104键盘才有)         0x5C
VK_APPS                AppsKey(104键盘才有)          0x5D

VK_NUMPAD0            小键盘 0 键                    0x60
VK_NUMPAD1            小键盘 1 键                    0x61
VK_NUMPAD2            小键盘 2 键                    0x62
VK_NUMPAD3            小键盘 3 键                    0x63
VK_NUMPAD4            小键盘 4 键                    0x64
VK_NUMPAD5            小键盘 5 键                    0x65
VK_NUMPAD6            小键盘 6 键                    0x66
VK_NUMPAD7            小键盘 7 键                    0x67
VK_NUMPAD8            小键盘 8 键                    0x68
VK_NUMPAD9            小键盘 9 键                    0x69

VK_F1 - VK_F24        功能键F1 – F24               0x70 - 0x87

VK_NUMLOCK            Num Lock 键                   0x90
VK_SCROLL             Scroll Lock 键                0x91

 所以,如果我们要检测鼠标左键按下,在定义了那个宏之后就可以这样写:

#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)  
//用来检测按键的点击事件

if (KEY_DOWN(VK_LBUTTON)) {  			//鼠标左键按下
		//要执行的语句
	}

 这句的意思是,当鼠标左键按下时,执行if里面的语句。

还有两个函数,可以获取鼠标当前相对于选定窗口的相对位置,它们是

BOOL GetCursorPos(LPPOINT lpPoint);
BOOL ScreenToClient(HWND hWnd, LPPOINT lpPoint);

第一个函数用来获取当前鼠标位置,第二个函数把鼠标位置转化为相对于某一句柄对应的窗口的鼠标相对位置。这个句柄当然就是我们程序窗口的句柄啦

不过,现在获取到的位置是像素级别的,不是很好操作。有一种方法可以把这个坐标转化为字符级别,也就是第 x 行第 y 列这样的形式。它就是

BOOL WINAPI GetCurrentConsoleFont(
HANDLE hConsoleOutput,
BOOL bMaximumWindow,
PCONSOLE_FONT_INFO lpConsoleCurrentFont
);

它可以把某一句柄所对应的窗口里的字体的长宽信息储存在一个字体信息结构体里面。

这样的话,我们只需要用鼠标位置和字符宽高做个除法就好了

到了这里,我们基本上已经实现目的了。来组合封装一下?

typedef struct mouse {	//鼠标型变量,包含两个int型变量,表示坐标
	int x;
	int y;
} mouse;

mouse WhereIClick() {
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);    //获取当前窗口句柄
	HWND h = GetForegroundWindow();                        //获取前台窗口句柄
	CONSOLE_FONT_INFO consoleCurrentFont;                //申请一个字体型结构体变量
	
	//----------移除快速编辑模式(对于win10用户)----------
	HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
	DWORD mode;
	GetConsoleMode(hStdin, &mode);
	mode &= ~ENABLE_QUICK_EDIT_MODE;
	SetConsoleMode(hStdin, mode);
    //快速编辑模式是一种很便捷的操作方式:左键选中,右键复制以及右键从剪贴板粘贴内容等
    //但是,如果鼠标选中控制台界面上的内容,控制台就被阻塞了,程序会暂停运行
    //所以要把它关掉

	//----------检测----------
    mouse click;
	if (KEY_DOWN(VK_LBUTTON)) {  			//鼠标左键按下
		POINT p;
		GetCursorPos(&p);
		ScreenToClient(h, &p);              //获取鼠标在窗口上的位置
		GetCurrentConsoleFont(hOutput, FALSE, &consoleCurrentFont); 	//获取字体信息
		click.x = p.x /= consoleCurrentFont.dwFontSize.X;				//矫正x坐标为字符尺度
		click.y = p.y /= consoleCurrentFont.dwFontSize.Y;				//矫正x坐标为字符尺度
	}
	return click;    //返回当前鼠标坐标
}

现在,按钮所需要的功能已经全部被制作出来了!可喜可贺,可喜可贺

只要先绘制好按钮,再循环检测鼠标位置,如果满足条件(即鼠标在按钮上按下)就执行相应动作,不就好了吗?


四、封装按钮

 激动人心的时刻就要来了。

我们想让按钮具有“数量可变”、“色彩可变”、“文字可变”、“位置可变”四个特征,那么我们的按钮函数至少需要一个数量参数,和三个分别代表色彩、颜色和位置的参数。由于参数是不确定个数的,所以我们还是使用可变参数函数。(还记得可变参数吗?第一节彩色输出里面说过)

 先把参数读取进来吧:

int DrawButton(int sum, ...) {	
	struct {					//建立按钮结构体
		char *line;            //按钮文字
		WORD color;            //WORD是一个32位无符号整数,也就是unsigned long
		mouse location;        //记得上面申请的mouse型结构体吗?
	} buttons[sum];
	
	va_list valist;
	va_start(valist, sum);								//指定可变参数起点
	for (int i = 0; i < sum; i++) {						//循环读取每一组按钮的数据
		buttons[i].line = va_arg( valist, char *);		//按钮文字
		buttons[i].color = va_arg( valist, int);		//按钮颜色
		buttons[i].location = va_arg( valist, mouse);	//按钮位置
	}
	va_end(valist);	
	
	for (int i = 0; i < sum; i++) {						//在指定位置显示第i个按钮
		SetWordAt(buttons[i].location.x, buttons[i].location.y);
		ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
	}
}

 然后就是最核心的判断按钮是否按下了:

while (1) {
		clickk = WhereIClick();						//当鼠标左键每按下一次,记录这时的坐标
		for (int i = 0; i < sum; i++) {				//循环检测所有按钮

			if (clickk.y == buttons[i].location.y && clickk.x > buttons[i].location.x && clickk.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被按下时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				system("cls");		//清屏
				return i;			//返回i,即被按下的按钮的编号
			} 
		}
		Sleep(50);	//检测鼠标按下的间隔50ms
	}

注意:clickk是一个mouse型的全局变量,这里没有说

完整的写一遍?

int DrawButton(int sum, ...) {	
	struct {					//建立按钮结构体
		char *line;            //按钮文字
		WORD color;            //WORD是一个32位无符号整数,也就是unsigned long
		mouse location;        //记得上面申请的mouse型结构体吗?
	} buttons[sum];
	
	va_list valist;
	va_start(valist, sum);								//指定可变参数起点
	for (int i = 0; i < sum; i++) {						//循环读取每一组按钮的数据
		buttons[i].line = va_arg( valist, char *);		//按钮文字
		buttons[i].color = va_arg( valist, int);		//按钮颜色
		buttons[i].location = va_arg( valist, mouse);	//按钮位置
	}
	va_end(valist);	
	
	for (int i = 0; i < sum; i++) {						//在指定位置显示第i个按钮
		SetWordAt(buttons[i].location.x, buttons[i].location.y);
		ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
	}

    while (1) {
		clickk = WhereIClick();						//当鼠标左键每按下一次,记录这时的坐标
		for (int i = 0; i < sum; i++) {				//循环检测所有按钮

			if (clickk.y == buttons[i].location.y && clickk.x > buttons[i].location.x && clickk.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被按下时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				system("cls");		//清屏
				return i;			//返回i,即被按下的按钮的编号
			} 
		}
		Sleep(50);	//检测鼠标按下的间隔50ms
	}
}

当第 i 个按钮被按下时,函数就会返回 i 。可以通过这个判断哪个按钮被按下了,由此决定执行什么操作 。

结束了?

结束了!

现在你已经实现了自定义按钮了!


五、附加功能:选中高亮,和颜色宏定义

生活中我们常见的按钮,在鼠标移动到上面的时候还会稍微改变一下外观,就像CSS的hover一样,给用户一个反馈。这个如何实现?

可以参照按钮判断的写法。逻辑是这样的:当鼠标坐标在某个按钮上,将其置1,并高亮显示;鼠标坐标移开,将其置0,并恢复原来的颜色。

具体实现如下(在原先的button函数里面修改):

mouse WhereIWillClick() {
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	HWND h = GetForegroundWindow();
	CONSOLE_FONT_INFO consoleCurrentFont;
	
	mouse click;
	//----------移除快速编辑模式(对于win10用户)----------
	HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
	DWORD mode;
	GetConsoleMode(hStdin, &mode);
	mode &= ~ENABLE_QUICK_EDIT_MODE;
	SetConsoleMode(hStdin, mode);
	
	//----------检测----------
		POINT p;
		GetCursorPos(&p);
		ScreenToClient(h, &p);              //获取鼠标在窗口上的位置
		GetCurrentConsoleFont(hOutput, FALSE, &consoleCurrentFont); 	//获取字体信息
		click.x = p.x /= consoleCurrentFont.dwFontSize.X;				//矫正x坐标为字符尺度
		click.y = p.y /= consoleCurrentFont.dwFontSize.Y;				//矫正x坐标为字符尺度

	return click;
}

int DrawButton(int sum, ...) {	
	struct {					//建立按钮结构体
		char *line;
		WORD color;
		mouse location;
	} buttons[sum];
	
	va_list valist;
	va_start(valist, sum);								//指定可变参数起点
	for (int i = 0; i < sum; i++) {						//循环读取每一组按钮的数据
		buttons[i].line = va_arg( valist, char *);		//按钮文字
		buttons[i].color = va_arg( valist, int);		//按钮颜色
		buttons[i].location = va_arg( valist, mouse);	//按钮位置
	}
	va_end(valist);	
	
	for (int i = 0; i < sum; i++) {						//在指定位置显示第i个按钮
		SetWordAt(buttons[i].location.x, buttons[i].location.y);
		ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
	}
	int sign[sum]={0};//为了选中/取消选中时不闪
	while (1) {
		clickk = WhereIClick();							//当鼠标左键每按下一次,记录这时的坐标
		not_click_yet=WhereIWillClick();				//记录本循环里鼠标的坐标,用来高亮按钮
		for (int i = 0; i < sum; i++) {					//循环检测所有按钮
			if (sign[i]==0&&not_click_yet.y == buttons[i].location.y && not_click_yet.x > buttons[i].location.x && not_click_yet.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被选中时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				//将第i个按钮高亮显示
				SetWordAt(buttons[i].location.x, buttons[i].location.y);
				ColorfulPrintf(WHITEBGBLACKFONT, "%s", buttons[i].line);
				sign[i]=1;
			} 
			if(sign[i]==1&&!(not_click_yet.y == buttons[i].location.y && not_click_yet.x > buttons[i].location.x && not_click_yet.x < (int)(buttons[i].location.x + strlen(buttons[i].line)))) {
				//如果没有被选中,取消高亮
				SetWordAt(buttons[i].location.x, buttons[i].location.y);
				ColorfulPrintf(buttons[i].color, "%s", buttons[i].line);
				sign[i]=0;
			}

			if (clickk.y == buttons[i].location.y && clickk.x > buttons[i].location.x && clickk.x < (int)(buttons[i].location.x + strlen(buttons[i].line))) {
				//第i个按钮被按下时(实现原理:当鼠标的y坐标等于该按钮,x坐标在字符串首尾范围内)
				system("cls");		//清屏
				return i;			//返回i,即被按下的按钮的编号
			} 
		}
		Sleep(50);	//检测鼠标按下的间隔50ms
	}
}

如你所见,这里新封装了一个 WhereIWillClick 函数,用来实时获取鼠标位置。具体的原理说明都在注释里啦!

为了更方便的描述颜色,节约查表的时间,我们还可以直接在宏中定义好一些颜色:

#define BLUE (FOREGROUND_INTENSITY | FOREGROUND_BLUE)
#define GREEN (FOREGROUND_INTENSITY | FOREGROUND_GREEN)
#define RED (FOREGROUND_INTENSITY | FOREGROUND_RED)
#define CYAN (FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_GREEN)
#define PURPLE (FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_RED)
#define YELLOW (FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED)
#define WHITEBGBLACKFONT ((BACKGROUND_INTENSITY | (BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED)) | (FOREGROUND_INTENSITY | !(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)))
#define DEFAULT (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)

这样的话,如果要调用按钮,只需要按以下格式使用这个函数即可:

mouse loc[5];
DrawButton(4, //按钮总数
    "一个按钮示例", RED, loc[0], //每个按钮的参数,一个按钮三组数据
    "两个按钮示例", YELLOW, loc[1], 
    "三个按钮示例", BLUE, loc[2], 
    "四个按钮示例", GREEN, loc[3]);

六、后记

在写这篇文的过程中,我参考过一些其它的文章,尤其是彩色输出和获取鼠标位置这两个函数。

Windows中使用C语言实现打印彩色文字到命令行窗口

当时参考的那篇获取鼠标位置那篇文章实在是找不到了[捂脸]等我找到了再补链接

本来我最初是没有打包出一个按钮函数的想法的,奈何发现这个东西真的挺好用,于是就打包出来了。一时调用一时爽,一直调用一直爽。

用着用着,突然就想看一看网上有没有人发布类似教程——至少我当时没有搜出来,所以也就有了这篇文章。毕竟互联网精神的核心是共享嘛,从别人的文章里学到了东西,自己也得写点什么东西出来丰富环境才好

希望能看到这里的你,在其他人有需要时,也能够伸出援手 :)

另外,如果本文有瑕疵、疏漏或者不严谨之处,还请大佬们在评论区留言提醒,批评指正。我会尽快更正

以上.

  • 18
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
要使用C语言解决凸包问题并实现动态效果,可以使用以下步骤: 1. 首先,需要使用凸包算法来计算给定点集的凸包。常见的凸包算法有Graham扫描算法和Jarvis步进算法。 2. 在计算凸包时,需要使用一个数据结构来存储点的坐标信息。可以使用C语言中的结构体来表示点,如下所示: ``` typedef struct { double x; double y; } Point; ``` 3. 在计算凸包时,需要将点按照一定的顺序进行排序。可以使用C语言中的qsort函数来实现,如下所示: ``` int cmp(const void* a, const void* b) { Point* p1 = (Point*)a; Point* p2 = (Point*)b; if (p1->x < p2->x) { return -1; } else if (p1->x > p2->x) { return 1; } else { return 0; } } qsort(points, n, sizeof(Point), cmp); ``` 4. 在计算凸包时,需要将点按照一定的顺序进行连接。可以使用C语言中的图形库来实现效果,如下所示: ``` void drawLine(Point p1, Point p2) { line(p1.x, p1.y, p2.x, p2.y); delay(100); } for (int i = 0; i < hullSize - 1; i++) { drawLine(hull[i], hull[i+1]); } drawLine(hull[hullSize-1], hull[0]); ``` 这里使用了一个自定义的drawLine函数来绘制线段,并使用了delay函数来实现动态效果。 完整的C语言实现代码如下所示: ``` #include <stdio.h> #include <stdlib.h> #include <graphics.h> #include <math.h> #define MAX_POINTS 1000 typedef struct { double x; double y; } Point; int cmp(const void* a, const void* b) { Point* p1 = (Point*)a; Point* p2 = (Point*)b; if (p1->x < p2->x) { return -1; } else if (p1->x > p2->x) { return 1; } else { return 0; } } double crossProduct(Point p1, Point p2, Point p3) { double x1 = p2.x - p1.x; double y1 = p2.y - p1.y; double x2 = p3.x - p2.x; double y2 = p3.y - p2.y; return x1 * y2 - x2 * y1; } void drawLine(Point p1, Point p2) { line(p1.x, p1.y, p2.x, p2.y); delay(100); } int main() { int gd = DETECT, gm; initgraph(&gd, &gm, ""); Point points[MAX_POINTS]; int n = 0; while (!kbhit() && n < MAX_POINTS) { if (ismouseclick(WM_LBUTTONDOWN)) { clearmouseclick(WM_LBUTTONDOWN); int x = mousex(); int y = mousey(); points[n].x = x; points[n].y = y; n++; putpixel(x, y, WHITE); } } qsort(points, n, sizeof(Point), cmp); Point hull[MAX_POINTS]; int hullSize = 0; for (int i = 0; i < n; i++) { while (hullSize >= 2 && crossProduct(hull[hullSize-2], hull[hullSize-1], points[i]) <= 0) { hullSize--; } hull[hullSize] = points[i]; hullSize++; } int hullStart = 0; for (int i = n - 2; i >= 0; i--) { while (hullSize >= hullStart + 2 && crossProduct(hull[hullSize-2], hull[hullSize-1], points[i]) <= 0) { hullSize--; } hull[hullSize] = points[i]; hullSize++; } for (int i = 0; i < hullSize - 1; i++) { drawLine(hull[i], hull[i+1]); } drawLine(hull[hullSize-1], hull[0]); getch(); closegraph(); return 0; } ``` 在运行程序时,可以使用鼠标点击来添加点,程序会自动计算凸包并进行可

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值