0


2024-键盘交互与计时器-实验3-题解

来自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;
}
    以上就是本人的解决方案,不过有一个缺陷,就是在持续左移或右移输出区域的时候会有画面撕裂现象,有解决该问题的同学可以评论,谢谢大家,本人大一学生,技术较低,大佬勿喷。

   

标签: c++

本文转载自: https://blog.csdn.net/qq_20753183/article/details/137970785
版权归原作者 Purity Dreemurr 所有, 如有侵权,请联系我们删除。

“2024-键盘交互与计时器-实验3-题解”的评论:

还没有评论