C++拾趣——绘制Console中DropdownMenu

大纲

  • 居中显示窗口
  • 清屏并重设光标
  • 绘制窗口
    • 绘制窗口顶部
    • 绘制下拉行
    • 绘制下拉框选项
    • 绘制按钮行
    • 绘制窗口底部
  • 修改终端默认行为
  • 对方向键的特殊处理
  • 过程控制
    • Tab键的处理
    • Enter键的处理
    • 上下左右方向键的处理
  • 完整代码
  • 代码地址

这次我们要绘制下拉菜单,如下图。

在这里插入图片描述

居中显示窗口

按照界面库的设计,Dropdown Menu不能独立存在,而是要位于一个窗口中。我们就用线段画出一个窗口。

这个窗口要位于终端中间,即居中。这样我们就需要先获取终端窗口的尺寸。

    // 获取终端窗口大小struct winsize w;ioctl(STDIN_FILENO, TIOCGWINSZ, &w);int terminalWidth = w.ws_col;int terminalHeight = w.ws_row;

然后通过当前窗口的大小,计算出起始绘制的坐标。下面options里保存的是可供选择的单选项,窗口的高度会随着选项个数而变化。

    // 窗口的宽度和高度int windowWidth = 40;int windowHeight = options.size() + 8;// 计算窗口的起始位置int startX = (terminalWidth - windowWidth) / 2;int startY = (terminalHeight - windowHeight) / 2;

清屏并重设光标

在开始绘制之前,我们需要使用《C++拾趣——绘制Console中圆形进度》中介绍的\033[2J\033[H来清屏并将光标移动到左上角。这是我们需要反复从最开始的位置重新绘制所有内容。

    cout << "\033[2J\033[H"; // 清屏并将光标移动到左上角

绘制窗口

绘制窗口顶部

在这里插入图片描述
因为要居中,所以在Y轴方向,我们先要输出一些\n来换行。

    // 打印顶部边框cout << string(startY, '\n');cout << string(startX, ' ') << "+" << string(windowWidth - 2, '-') << "+" << endl;

绘制下拉行

在这里插入图片描述
下拉行是未展开时的下拉框。我们可以通过Tab键切换焦点,让其被选中。
在这里插入图片描述

    // 打印下拉框行cout << string(startX, ' ') << "| ";if (isDropdownSelected) {cout << greenBackground << whiteText << "[Select Option]" << reset;} else {cout << "[Select Option]";}cout << string(windowWidth - 4 - 15, ' ') << " |" << endl;

绘制下拉框选项

在这里插入图片描述
下拉框选项是展开状态。我们可以对下拉行进行Enter操作,来让下拉框展开或者缩回。

    // 打印下拉框选项if (isDropdownOpen) {for (int i = 0; i < options.size(); ++i) {cout << string(startX, ' ') << "| ";if (i == selectedIndex) {cout << redBackground << whiteText << options[i] << reset;} else {cout << options[i];}cout << string(windowWidth - 4 - options[i].size(), ' ') << " |" << endl;}} else {cout << string(startX, ' ') << "| " << string(windowWidth - 4, ' ') << " |" << endl;}

绘制按钮行

在这里插入图片描述
我们需要绘制两个按钮:OK和Cancel。

因为按钮也要居中显示,所以我们先需要算出要空出多少列出来——padding值。

当按钮被选中时,它会变成绿色背景,另外一个按钮就会变成默认背景。

    // 打印按钮行string okButton = "[ OK ]";string cancelButton = "[Cancel]";int totalButtonLength = okButton.length() + cancelButton.length() + 2; // 2 spaces between buttonsint padding = (windowWidth - totalButtonLength) / 2;cout << string(startX, ' ') << "| " << string(padding, ' ');if (isButtonSelected && buttonIndex == 0) {cout << greenBackground << whiteText << blinkText << okButton << reset;} else {cout << okButton;}cout << "  ";if (isButtonSelected && buttonIndex == 1) {cout << greenBackground << whiteText << blinkText << cancelButton << reset;} else {cout << cancelButton;}cout << string(windowWidth - totalButtonLength - padding - 2 - 2, ' ') << " |" << endl;

绘制窗口底部

在这里插入图片描述

    // 打印底部边框cout << string(startX, ' ') << "+" << string(windowWidth - 2, '-') << "+" << endl;

修改终端默认行为

由于不能鼠标操作,所以我们只能通过键盘操作界面,比如进行Button的选择和按下操作。

我们只接受Tab、向左、向右、向上、向下、ESC和Enter等7个键。

Tab键可以在Dropdown Menu和Push Button之间切换;

向左、向右只能在Push Button间切换;

向上、向下只能在Dropdown Menu间切换;

ESC是退出程序;

Enter表示PushButton被按下,或者Dropdown Menu被选中。

默认情况下,我们在键盘上输出的可见字符都会显示在终端上。但是在当前场景下,我们并不希望有这样的效果。这就需要我们修改终端的默认行为。

下面的代码在将终端修改为静默模式(关闭规范模式(ICANON)和回显(ECHO),使得输入字符不需要按下回车键就能立即读取,并且输入的字符不会显示在终端上)后,等待并读取键盘的输入,然后再还原原始的设置。这步还原操作非常重要,否则程序退出后,终端会一直处于静默模式,后续的输入都不会显示在终端上。这也预示着,如果本程序使用ctrl+c强行终止,会出现终端行为异常的情况。

int getch() {struct termios oldt, newt;int ch;tcgetattr(STDIN_FILENO, &oldt);newt = oldt;newt.c_lflag &= ~(ICANON | ECHO);   // 修改终端设置:关闭回显和规范模式tcsetattr(STDIN_FILENO, TCSANOW, &newt);    // 设置终端为静默模式ch = getchar();……tcsetattr(STDIN_FILENO, TCSANOW, &oldt);return ch;
}

对方向键的特殊处理

在终端环境中,特殊键(如箭头键、功能键等)通常不会直接生成单个字符的输入,而是生成一系列字符的序列。对于箭头键,通常的序列是以 ESC 键(ASCII 码 27)开头,后面跟着一个或多个字符来表示具体的键。

  • 向上箭头:ESC + [ + A
  • 向下箭头:ESC + [ + B
  • 向右箭头:ESC + [ + C
  • 向左箭头:ESC + [ + D
    ch = getchar();if (ch == 27) { // 如果是ESC键ch = getchar();if (ch == 91) { // 如果是'['键ch = getchar();if (ch == 65) ch = 1000; // 上箭头键if (ch == 66) ch = 1001; // 下箭头键if (ch == 67) ch = 1002; // 右箭头键if (ch == 68) ch = 1003; // 左箭头键}}

过程控制

下面代码会在getch()中等待终端的输入。

int main() {bool isDropdownSelected = false;bool isDropdownOpen = false;vector<string> options = {"Option 1", "Option 2", "Option 3", "Option 4"};int selectedIndex = 0;bool isButtonSelected = false;int buttonIndex = 0;while (true) {display(isDropdownSelected, isDropdownOpen, options, selectedIndex, isButtonSelected, buttonIndex);int ch = getch();if (ch == 9) { // Tab 键if (!isDropdownSelected && !isButtonSelected) {isDropdownSelected = true;} else if (isDropdownSelected) {isDropdownSelected = false;isButtonSelected = true;buttonIndex = 0; // 默认选择 OK 按钮} else if (isButtonSelected) {if (buttonIndex == 1) {isButtonSelected = false;isDropdownSelected = true;} else {buttonIndex = (buttonIndex + 1) % 2;}}} else if (ch == 10 || ch == 13) { // Enter 键if (isDropdownSelected) {isDropdownOpen = !isDropdownOpen;} else if (isButtonSelected) {cout << "\033[2J\033[H"; // 清空画面if (buttonIndex == 0) { // OK 按钮cout << "You selected: " << options[selectedIndex] << endl;} else if (buttonIndex == 1) { // Cancel 按钮cout << "Operation cancelled." << endl;}break;}} else if (ch == 1000) { // 上箭头键if (isDropdownOpen && selectedIndex > 0) {selectedIndex--;}} else if (ch == 1001) { // 下箭头键if (isDropdownOpen && selectedIndex < options.size() - 1) {selectedIndex++;}} else if (ch == 1002) { // 右箭头键if (isButtonSelected) {buttonIndex = (buttonIndex + 1) % 2;}} else if (ch == 1003) { // 左箭头键if (isButtonSelected) {buttonIndex = (buttonIndex + 1) % 2;}} else if (ch == 27) { // Esc 键退出cout << "\033[2J\033[H"; // 清空画面break;}}return 0;
}

Tab键的处理

该界面分为三个个区域:Dropdown Menu行区域、Dropdown Menu选项区域和Push Button区域。isButtonSelected变量表示当前焦点是否在Push Button区域;isDropdownSelected表示当前焦点是否在Dropdown Menu行区域;isDropdownOpen表示当前焦点是否在Dropdown Menu选项区域。

如果是Push Button区域(isButtonSelected == true),且当前聚焦的是Cancel(buttonIndex == 1),则在收到Tab键时,会跳到Dropdown Menu行区域(isDropdownSelected= true);如果聚焦的不是Cancel,则通过buttonIndex = (buttonIndex + 1) % 2计算出要聚焦到的Push Button的下标。

如果是Dropdown Menu行区域或者Dropdown Menu选项区域(isDropdownSelected== true),则在收到Tab键时,跳到Push Button区域(isButtonSelected == true)的OK按钮处。这就预示着光标在下拉框选项中时,Tab键并不会切换下拉选项。

在这里插入图片描述

Enter键的处理

如果当前聚焦的是Push Button区域(isButtonSelected == true),则判断聚焦的是OK还是Cancel。如果是OK,则输出用户的选项并退出;如果是Cancel则直接退出。

如果当前聚焦的是Dropdown Menu行区域(isButtonSelected == false),则会根据当前是否为展开状态决定后续是要展开还是收缩选项

在这里插入图片描述

上下左右方向键的处理

我们让上下键只能在Dropdown Menu区域切换焦点,左右键只能在Push Button区域切换焦点。这是因为Dropdown Menu区域中的项目是上下布局的,而Push Button区域是左右布局的。

在这里插入图片描述

完整代码

#include <iostream>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <vector>using namespace std;void display(bool isDropdownSelected, bool isDropdownOpen, const vector<string>& options, int selectedIndex, bool isButtonSelected, int buttonIndex) {const string greenBackground = "\033[42m"; // 绿色背景const string redBackground = "\033[41m";   // 红色背景const string whiteText = "\033[37m";       // 白色字体const string blinkText = "\033[5m";        // 闪烁文本const string reset = "\033[0m";            // 重置颜色// 获取终端窗口大小struct winsize w;ioctl(STDIN_FILENO, TIOCGWINSZ, &w);int terminalWidth = w.ws_col;int terminalHeight = w.ws_row;// 窗口的宽度和高度int windowWidth = 40;int windowHeight = 10;// 计算窗口的起始位置int startX = (terminalWidth - windowWidth) / 2;int startY = (terminalHeight - windowHeight) / 2;cout << "\033[2J\033[H"; // 清屏并将光标移动到左上角// 打印顶部边框cout << string(startY, '\n');cout << string(startX, ' ') << "+" << string(windowWidth - 2, '-') << "+" << endl;// 打印下拉框行cout << string(startX, ' ') << "| ";if (isDropdownSelected) {cout << greenBackground << whiteText << "[Select Option]" << reset;} else {cout << "[Select Option]";}cout << string(windowWidth - 4 - 15, ' ') << " |" << endl;// 打印下拉框选项if (isDropdownOpen) {for (int i = 0; i < options.size(); ++i) {cout << string(startX, ' ') << "| ";if (i == selectedIndex) {cout << redBackground << whiteText << options[i] << reset;} else {cout << options[i];}cout << string(windowWidth - 4 - options[i].size(), ' ') << " |" << endl;}} else {cout << string(startX, ' ') << "| " << string(windowWidth - 4, ' ') << " |" << endl;}// 打印按钮行string okButton = "[ OK ]";string cancelButton = "[Cancel]";int totalButtonLength = okButton.length() + cancelButton.length() + 2; // 2 spaces between buttonsint padding = (windowWidth - totalButtonLength) / 2;cout << string(startX, ' ') << "| " << string(padding, ' ');if (isButtonSelected && buttonIndex == 0) {cout << greenBackground << whiteText << blinkText << okButton << reset;} else {cout << okButton;}cout << "  ";if (isButtonSelected && buttonIndex == 1) {cout << greenBackground << whiteText << blinkText << cancelButton << reset;} else {cout << cancelButton;}cout << string(windowWidth - totalButtonLength - padding - 2 - 2, ' ') << " |" << endl;// 打印底部边框cout << string(startX, ' ') << "+" << string(windowWidth - 2, '-') << "+" << endl;cout << reset; // 重置颜色
}int getch() {struct termios oldt, newt;int ch;tcgetattr(STDIN_FILENO, &oldt);newt = oldt;newt.c_lflag &= ~(ICANON | ECHO);tcsetattr(STDIN_FILENO, TCSANOW, &newt);ch = getchar();if (ch == 27) { // 如果是ESC键ch = getchar();if (ch == 91) { // 如果是'['键ch = getchar();if (ch == 65) ch = 1000; // 上箭头键if (ch == 66) ch = 1001; // 下箭头键if (ch == 67) ch = 1002; // 右箭头键if (ch == 68) ch = 1003; // 左箭头键}}tcsetattr(STDIN_FILENO, TCSANOW, &oldt);return ch;
}int main() {bool isDropdownSelected = false;bool isDropdownOpen = false;vector<string> options = {"Option 1", "Option 2", "Option 3", "Option 4"};int selectedIndex = 0;bool isButtonSelected = false;int buttonIndex = 0;while (true) {display(isDropdownSelected, isDropdownOpen, options, selectedIndex, isButtonSelected, buttonIndex);int ch = getch();if (ch == 9) { // Tab 键if (!isDropdownSelected && !isButtonSelected) {isDropdownSelected = true;} else if (isDropdownSelected) {isDropdownSelected = false;isButtonSelected = true;buttonIndex = 0; // 默认选择 OK 按钮} else if (isButtonSelected) {if (buttonIndex == 1) {isButtonSelected = false;isDropdownSelected = true;} else {buttonIndex = (buttonIndex + 1) % 2;}}} else if (ch == 10 || ch == 13) { // Enter 键if (isDropdownSelected) {isDropdownOpen = !isDropdownOpen;} else if (isButtonSelected) {cout << "\033[2J\033[H"; // 清空画面if (buttonIndex == 0) { // OK 按钮cout << "You selected: " << options[selectedIndex] << endl;} else if (buttonIndex == 1) { // Cancel 按钮cout << "Operation cancelled." << endl;}break;}} else if (ch == 1000) { // 上箭头键if (isDropdownOpen && selectedIndex > 0) {selectedIndex--;}} else if (ch == 1001) { // 下箭头键if (isDropdownOpen && selectedIndex < options.size() - 1) {selectedIndex++;}} else if (ch == 1002) { // 右箭头键if (isButtonSelected) {buttonIndex = (buttonIndex + 1) % 2;}} else if (ch == 1003) { // 左箭头键if (isButtonSelected) {buttonIndex = (buttonIndex + 1) % 2;}} else if (ch == 27) { // Esc 键退出cout << "\033[2J\033[H"; // 清空画面break;}}return 0;
}

代码地址

https://github.com/f304646673/cpulsplus/tree/master/console_ui/dropdownmenu

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1558233.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

20241008深度学习动手篇

文章目录 1.如何写一个神经网络进行训练?1.1创建一个子类,搭建你需要的神经网络结构1.2 加载数据集1.3 自定义一些指标评估函数1.4训练1.5 结果展示 2.参考文献 1.如何写一个神经网络进行训练? 1.1创建一个子类,搭建你需要的神经网络结构 # File: 241008LeNet.py # Author:…

“炫我”受邀出席虚拟现实及元宇宙产业创新论坛!

当前&#xff0c;新一轮科技革命和产业变革向纵深演进&#xff0c;虚拟现实及元宇宙等相关产业加速发展&#xff0c;催生了新产业新业态新模式&#xff0c;发展潜力巨大、应用前景广阔。 9月27日&#xff0c;由北京市科学技术委员会、中关村科技园区管理委员会&#xff0c;北京…

什么是变阻器?

变阻器是一种电子元件&#xff0c;主要用于调整电路中的电阻值&#xff0c;从而实现对电流、电压等电学参数的控制。它在电路中起到非常重要的作用&#xff0c;广泛应用于各种电子设备和实验装置中。 变阻器的主要作用是改变电路中的电阻值。在电路中&#xff0c;电阻值的大小…

基于springboot vue 学生就业信息管理系统设计与实现

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作☆☆☆ 精彩专栏推荐订阅☆☆☆☆…

LC538 - 把二叉搜索树转换为累加树

文章目录 1 题目2 思路3 ACM模式参考 1 题目 https://leetcode.cn/problems/convert-bst-to-greater-tree/description/ 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&#xff08;Greater Sum Tree&#xff09; 累加树&#…

递归特征消除(RFE)与随机森林回归模型的 MATLAB 实现

在机器学习中&#xff0c;特征选择是提高模型性能的重要步骤。本文将详细探讨使用递归特征消除&#xff08;RFE&#xff09;结合随机森林回归模型的实现&#xff0c;以研究对股票收盘价影响的特征。我们将逐步分析代码并介绍相关的数学原理。 1. 数据准备 首先&#xff0c;我…

wordpress发邮件SMTP服务器配置步骤指南?

wordpress发邮件功能如何优化&#xff1f;怎么用wordpress发信&#xff1f; 由于WordPress默认的邮件发送功能可能不够稳定&#xff0c;配置SMTP服务器成为了许多网站管理员的选择。AokSend将详细介绍如何在WordPress中配置SMTP服务器&#xff0c;以确保邮件能够顺利发送。 w…

注册安全分析报告:惠农网

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

BLE MESH学习2——自定义MESH网络架构思考

BLE MESH学习2——自定义MESH网络架构思考 基于对WCH CH582这款单片机的了解&#xff0c;其可以实现mesh配网、朋友节点、低功耗节点和中继节点的角色&#xff0c;基本功能无问题。在此基础上&#xff0c;考虑满足IoT需求的MESH架构设计&#xff0c;作为后续设计的“白皮书”。…

第170天:应急响应-战中溯源反制对抗上线CSGoby蚁剑Sqlmap等安全工具

目录 案例一&#xff1a;溯源反制-Webshell工具-Antsword 案例二&#xff1a;溯源反制-SQL注入工具-SQLMAP 案例三&#xff1a;溯源反制-漏洞扫描工具-Goby 案例四&#xff1a;溯源反制-远程控制工具-CobaltStrike 反制Server&#xff0c;爆破密码&#xff08;通用&#x…

快餐食品检测系统源码分享[一条龙教学YOLOV8标注好的数据集一键训练_70+全套改进创新点发刊_Web前端展示]

快餐食品检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

sql堆叠注入

准备知识&#xff1a; php中multi_query()&#xff1a;一次可以执行多个sql语句比如&#xff1a;查询注入id1&#xff1b;update xxx; 定义&#xff1a;如果后端代码中&#xff0c;数据库执行的方法是multi_query()&#xff0c;那么就可以一次执行多个sql&#xff0c;也就可以…

java面向对之象类的继承与多态

目录 1.类的继承 图解 案例:创建一个动物类和一个猫类 1.代码 1)动物类 2)猫类 3.测试类 2.效果 2.父类方法的重写 案例:如何重写父类的方法 1.代码 1&#xff09;Animal类 2&#xff09;Dog类 3&#xff09;测试类 2.效果 3.super关键字 案例:如何在子类中调用父类的方…

肺结节分割与提取系统(基于传统图像处理方法)

Matlab肺结节分割(肺结节提取)源程序&#xff0c;GUI人机界面版本。使用传统图像分割方法&#xff0c;非深度学习方法。使用LIDC-IDRI数据集。 工作如下&#xff1a; 1、读取图像。读取原始dicom格式的CT图像&#xff0c;并显示&#xff0c;绘制灰度直方图&#xff1b; 2、图像…

系统架构设计师论文《论企业集成平台的理解与应用》精选试读

论文真题 企业集成平台&#xff08;Enterprise Imtcgation Plaform,EIP)是支特企业信息集成的像环境&#xff0c;其主要功能是为企业中的数据、系统和应用等多种对象的协同行提供各种公共服务及运行时的支撑环境。企业集成平台能够根据业务模型的变化快速地进行信息系统的配置…

使用XML实现MyBatis的基础操作

目录 前言 1.准备工作 1.1⽂件配置 1.2添加 mapper 接⼝ 2.增删改查操作 2.1增(Insert) 2.2删(Delete) 2.3改(Update) 2.4查(Select) 前言 接下来我们会使用的数据表如下&#xff1a; 对应的实体类为&#xff1a;UserInfoMapper 所有的准备工作都在如下文章。 MyBati…

github创建仓库并本地使用流程,以及问题src refspec xxx does not match any

1.在 GitHub 上创建一个新仓库 登录你的 GitHub 账户。 点击右上角的 “” 按钮&#xff0c;然后选择 “New repository”。 填写仓库名称&#xff08;如 my-repo&#xff09;。 &#xff08;可选&#xff09;添加描述&#xff0c;选择是否公开或私有仓库。 &#xff08;可选&…

电层相关 -- Transponder Muxponder

光波长转换类单板&#xff08;Optical Transponder Unit&#xff0c;简称OTU单板&#xff09;主要将客户侧业务经过封装映射、汇聚等处理后&#xff0c;输出符合WDM系统要求的标准波长的光信号。OTU的主要功能有两类&#xff1a;Transponder 和Muxponder&#xff0c;简称TP和MP…

Python 字典(Dictionary) items(),pop(‘key‘)方法

描述 Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。 语法 items()方法语法&#xff1a; dict.items()参数 NA。 返回值 返回可遍历的(键, 值) 元组数组。 实例 以下实例展示了 items()函数的使用方法&#xff1a; tinydict {Google: …

使用Docker搭建WAF-开源Web防火墙VeryNginx

1、说明 VeryNginx 基于 lua_nginx_module(openrestry) 开发,实现了防火墙、访问统计和其他的一些功能。 集成在 Nginx 中运行,扩展了 Nginx 本身的功能,并提供了友好的 Web 交互界面。 文章目录 1、说明1.1、基本概述1.2、主要功能1.3、应用场景2、拉取镜像3、配置文件4、…