来自C++专业课的第三次作业:
实验要求:
模拟一个办事机构(如银行)的叫号程序。
在一个显示区域内从上到下按顺序显示5个号码,最开始是1-5。四个方向键控制显示区域的移动。空格键产生一个新号码,将最前面的号码挤出显示区。ESC键退出系统。
使用键盘交互与计时器实现该程序,使用容器装载号码。
最开始想抄一下某学长的题解,但是有非常严重的闪烁现象,且代码弱注释,十分难以理解,不过还是看完了,在此感谢那位不知名学长,并附上参考链接:
第三次实验:键盘交互与计时器-容器_辩之竹计时器键盘怎么用-CSDN博客
源代码运行的效果:(两帧不同的画面)
可以看到,通过截图软件对不同帧进行截图,可以比较明显的捕捉到输出的频闪现象,从用户交互的方面讲,是不友好的。
分析实验要求,需要使用一个容器,容器保存序列信息,并且使用类封装,具有打印,更新队列的功能。
首先选择合适的容器是很重要的,由于实验数据强度很小,只有五行,因此不用考虑容器的性能问题,只需要找一个便于操作的容器即可。而容器有很多种,常见的有vector、deque、list、queue等,而我希望可以这个容器具有从尾部插入数据,从头部弹出数据的功能,则选择deque较为合适,且deque可以使用下标访问而无需迭代器,更加方便(其实是不太会用iterator)
编写的类如下:
class list
{
private:
deque<int>info; //新建一个deque info
public:
list() //构造函数,负责向info中压入从1-5的序号
{
for(int i=1;i<6;i++)
{
this->info.push_back(i);
}
}
void update() //更新序列信息
{
this->info.pop_front(); //弹出第一个序号
this->info.push_back(this->info.back()+1); //压入后一个序号
}
void show(int x,int y) //打印序列信息函数,这里传入的x,y是输出位置,因为实验要求通过键盘控制打印区域的位置
{
SetOutputPosition(x,y); //移动光标到指定位置
printf("┏━━━━━━━┓"); //打印外部框架
for(int i=1;i<6;i++)
{
SetOutputPosition(x,y+i); //每打印一行,光标下移一行
printf("┃%-7d┃",this->info[i-1]); //printf自定义输出,左对齐,固定占用7个空格
}
SetOutputPosition(x,y+6); //再下移一行
printf("┗━━━━━━━┛"); //打印外部框架
}
};
这里要注意,deque需要引用deque头文件。
下面编写接收键盘的类,负责接收键盘信息,并且记录光标的移动位置。
控制台光标坐标系示意图:
首先要明确,计算机记录键盘信息有多种方式,如果要使用计时器实现键盘触发记录,那么就是通过记录键盘状态,并通过计时器计时,当按下某个键的时间达到某一个阈值,则识别为一次触发,否则不触发,也就是本实验的要求。(其实还有另一种方式,就是通过检测键盘状态切换识别为触发,这种方式又分为按下触发和弹起触发,即键盘按下识别为一次触发或键盘弹起识别为一次触发,如果键盘状态没有改变,那么不记录触发)。
键盘类如下:
const int FPS=100;
const int edge_x=100;
const int edge_y=30;
class display{ //键盘类(显示类)
private:
list l; //类内定义一个list对象,即存储序列信息的对象
clock_t clk_1; //定义一个计时器变量,用于记录时间信息(ms单位)
int x,y,pre_x,pre_y;
bool flag; //标记位
public:
display(int X,int Y) //构造函数
{
this->x=X;
this->y=Y;
clk_1=clock(); //clk_1记录初始时间,并启动计时器
}
void erase(int x,int y) //擦除函数
{
for(int i=0;i<7;i++) //通过在指定位置输出空格覆盖之前的信息,而不使用cls,可以达到更高的性能
{
SetOutputPosition(x,y+i); //光标移动至该行初始位置
printf(" "); //打印空格覆盖
} //这里不使用\n的原因:x方向也有偏移,如果使用回车,会导致下一行光标移动至x=0位置
}
void refresh(int x,int y) //刷新函数,重新打印新的信息
{
SetOutputPosition(x,y); //光标移动
l.show(x,y); //通过l成员函数打印信息
}
void show() //键盘检测函数
{
if(clock()-this->clk_1>1000/FPS) //通过FPS计算触发时长,如果计时器当前的时间和clk_1记录的时间超过每一帧的时间间隔,则检测键盘状态
{
if(GetAsyncKeyState(VK_LEFT)&&x>0) //按下左键,并且x未到左边缘
{
pre_x=x; //记录下当前坐标信息
pre_y=y;
x--; //更新坐标
erase(pre_x,pre_y); //擦除更新前的打印
refresh(x,y); //在更新后的坐标处重新打印
}
if(GetAsyncKeyState(VK_RIGHT)&&x+5<edge_x) //按下右键,并且x未到右边缘
{
pre_x=x;
pre_y=y;
x++;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_UP)&&y>0)
{
pre_x=x;
pre_y=y;
y--;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_DOWN)&&y+5<edge_y)
{
pre_x=x;
pre_y=y;
y++;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_ESCAPE)) //按下ESC?
{
exit(0); //退出程序,代码0
}
this->clk_1=clock(); //该轮键盘检测结束后更新clk_1的值为当前时间
}
if(GetAsyncKeyState(VK_SPACE)&&flag==0) //按下空格,并且标志位为0,即之前是没有按下的
{
flag=1; //更新状态为已按下
this->l.update(); //更新序列信息
erase(x,y);
refresh(x,y); //重新打印
}
if(!GetAsyncKeyState(VK_SPACE)) //如果没有按下空格
{
flag=0; //更新信息为未按下
}
}
void start() //启动打印函数
{
flag=0; //空格键状态未按下
refresh(x,y); //先打印出初始列表
while(1)
{
show(); //持续键盘检测
}
}
};
这样就可以实现方向键的控制,且基本上是实时响应的,可以自定义键盘检测刷新率和输出边界, 在没有键盘操作的情况下不进行画面刷新,避免了画面持续频闪,并且提高了程序响应速度。
但是对于空格键来说,有一点小小的问题,虽然方向键按帧率相应,但是空格键控制列表的刷新,也就是说我们希望列表在我们按下空格键时只更新一次,而不是按下一次空格,空格按帧率识别了多次,列表也更新多次。
对于空格键,需要使用上述的第二种检测方式,也就是按帧率不断记录空格键的状态,只有检测到当前帧的空格按下并且上一帧的空格没有按下时,才进行一次列表更新,其他情况均忽略,这样不管按下键盘的持续时间有多长,最终列表只更新一次,要再更新一次的话,就松开空格重新按下。
两种检测方案区别:
最终效果:
![](https://img-blog.csdnimg.cn/direct/74d251d87bbd433f89a1aef9e419d632.png)
完整代码:
/*
PurityDreemurr
2024-04-19
HITWH
*/
#include<iostream>
#include<cstdio>
#include<deque>
#include<cstdlib>
#include<ctime>
#include<conio.h>
#include<windows.h>
using namespace std;
const int FPS=100;
const int edge_x=100;
const int edge_y=30;
void SetOutputPosition(int x, int y)//设置输出坐标
{
HANDLE h;//接收控制台输出设备
h = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos;//获取控制台坐标
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(h, pos);//设置控制台光标位置
}
class list
{
private:
deque<int>info; //新建一个deque info
public:
list() //构造函数,负责向info中压入从1-5的序号
{
for(int i=1;i<6;i++)
{
this->info.push_back(i);
}
}
void update() //更新序列信息
{
this->info.pop_front(); //弹出第一个序号
this->info.push_back(this->info.back()+1); //压入后一个序号
}
void show(int x,int y) //打印序列信息函数,这里传入的x,y是输出位置,因为实验要求通过键盘控制打印区域的位置
{
SetOutputPosition(x,y); //移动光标到指定位置
printf("┏━━━━━━━┓"); //打印外部框架
for(int i=1;i<6;i++)
{
SetOutputPosition(x,y+i); //每打印一行,光标下移一行
printf("┃%-7d┃",this->info[i-1]); //printf自定义输出,左对齐,固定占用7个空格
}
SetOutputPosition(x,y+6); //再下移一行
printf("┗━━━━━━━┛"); //打印外部框架
}
};
class display{ //键盘类(显示类)
private:
list l; //类内定义一个list对象,即存储序列信息的对象
clock_t clk_1; //定义一个计时器变量,用于记录时间信息(ms单位)
int x,y,pre_x,pre_y;
bool flag; //标记位
public:
display(int X,int Y) //构造函数
{
this->x=X;
this->y=Y;
clk_1=clock(); //clk_1记录初始时间,并启动计时器
}
void erase(int x,int y) //擦除函数
{
for(int i=0;i<7;i++) //通过在指定位置输出空格覆盖之前的信息,而不使用cls,可以达到更高的性能
{
SetOutputPosition(x,y+i); //光标移动至该行初始位置
printf(" "); //打印空格覆盖
} //这里不使用\n的原因:x方向也有偏移,如果使用回车,会导致下一行光标移动至x=0位置
}
void refresh(int x,int y) //刷新函数,重新打印新的信息
{
SetOutputPosition(x,y); //光标移动
l.show(x,y); //通过l成员函数打印信息
}
void show() //键盘检测函数
{
if(clock()-this->clk_1>1000/FPS) //通过FPS计算触发时长,如果计时器当前的时间和clk_1记录的时间超过每一帧的时间间隔,则检测键盘状态
{
if(GetAsyncKeyState(VK_LEFT)&&x>0) //按下左键,并且x未到左边缘
{
pre_x=x; //记录下当前坐标信息
pre_y=y;
x--; //更新坐标
erase(pre_x,pre_y); //擦除更新前的打印
refresh(x,y); //在更新后的坐标处重新打印
}
if(GetAsyncKeyState(VK_RIGHT)&&x+5<edge_x) //按下右键,并且x未到右边缘
{
pre_x=x;
pre_y=y;
x++;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_UP)&&y>0)
{
pre_x=x;
pre_y=y;
y--;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_DOWN)&&y+5<edge_y)
{
pre_x=x;
pre_y=y;
y++;
erase(pre_x,pre_y);
refresh(x,y);
}
if(GetAsyncKeyState(VK_ESCAPE)) //按下ESC?
{
exit(0); //退出程序,代码0
}
this->clk_1=clock(); //该轮键盘检测结束后更新clk_1的值为当前时间
}
if(GetAsyncKeyState(VK_SPACE)&&flag==0) //按下空格,并且标志位为0,即之前是没有按下的
{
flag=1; //更新状态为已按下
this->l.update(); //更新序列信息
erase(x,y);
refresh(x,y); //重新打印
}
if(!GetAsyncKeyState(VK_SPACE)) //如果没有按下空格
{
flag=0; //更新信息为未按下
}
}
void start() //启动打印函数
{
flag=0; //空格键状态未按下
refresh(x,y); //先打印出初始列表
while(1)
{
show(); //持续键盘检测
}
}
};
int main()
{
display dsp1(0,0); //新建键盘类
dsp1.start(); //启动
return 0;
}
以上就是本人的解决方案,不过有一个缺陷,就是在持续左移或右移输出区域的时候会有画面撕裂现象,有解决该问题的同学可以评论,谢谢大家,本人大一学生,技术较低,大佬勿喷。
版权归原作者 Purity Dreemurr 所有, 如有侵权,请联系我们删除。