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.mzph.cn/bicheng/55833.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JavaEE】【多线程】Thread类讲解

目录 Thread构造方法Thread 的常见属性创建一个线程获取当前线程引用终止一个线程使用标志位使用自带的标志位 等待一个线程线程休眠线程状态线程安全线程不安全原因总结解决由先前线程不安全问题例子 Thread构造方法 方法说明Thread()创建线程对象Thread(Runnable target)使用…

论文速读:基于渐进式转移的无监督域自适应舰船检测

这篇文章的标题是《Unsupervised Domain Adaptation Based on Progressive Transfer for Ship Detection: From Optical to SAR Images》基于渐进式转移的无监督域自适应舰船检测:从光学图像到SAR图像&#xff0c;作者是Yu Shi等人。文章发表在IEEE Transactions on Geoscience…

pg if条件语句

1.语法&#xff1a; 2.区别 IF 语句&#xff1a; 只能在 PL/pgSQL 中使用&#xff0c;不适合在直接的 SQL 查询中使用。没有返回值&#xff0c;仅仅是控制逻辑流程。适合用在存储过程、函数和触发器中。 CASE 语句&#xff08;在 PL/pgSQL 中&#xff09;&#xff1a; 可以在 P…

Comfyui segmentAnythingUltra V2报错

&#x1f385;问题表现及解决方案 Comfyui segmentAnythingUltra V2报错&#xff0c;找不到VITMatte模型&#xff0c;这个报错报的比较模糊&#xff0c;所以花了一点时间找模型。 简单来说&#xff0c;到huggingface上&#xff1a; https://huggingface.co/hustvl/vitmatte-s…

线性回归损失函数的推导

要推导损失函数公式 ℓ ( θ ) 1 2 n ( y ^ − y ) ⊤ ( y ^ − y ) \ell(\boldsymbol{\theta}) \frac{1}{2n}(\hat{\boldsymbol{y}} - \boldsymbol{y})^\top(\hat{\boldsymbol{y}} - \boldsymbol{y}) ℓ(θ)2n1​(y^​−y)⊤(y^​−y)&#xff0c;我们可以从几个基础概念开…

2024 Mysql基础与进阶操作系列之MySQL触发器详解(20)作者——LJS[你个小黑子这都还学不会嘛?你是真爱粉嘛?真是的 ~;以后请别侮辱我家鸽鸽]

欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️ MYSQL REDIS Advance operation 专栏跑道二➡️ 24 Network Security -LJS ​ ​ ​ 专栏跑道三 ➡️HCIP&#xff1b;H3C-SE;CCIP——…

Qt 图片显示 动态选择图片显示

在 Qt 中&#xff0c;显示图片通常使用 QLabel 和 QPixmap 进行图像的加载和显示。QPixmap 是专门用于显示图像的类&#xff0c;而 QLabel 则是一个可以容纳图片的小部件。 1、使用 QLabel 和 QPixmap 来显示图片&#xff1a; #include <QApplication> #include <QL…

windows配置java环境变量

windows如何配置java环境变量&#xff08;java环境变量配置教程&#xff09;_windowsjava环境变量配置-CSDN博客

Redis接口访问优化

说明&#xff1a;之前写过一篇使用Redis接口访问的博客&#xff0c;如下。最近有相关需求&#xff0c;把代码拿出来后&#xff0c;做了一些优化&#xff0c;挺有意思的&#xff0c;本文介绍在原基础上 使用Redis实现接口防抖 优化 总的来说&#xff0c;这次使用Redis实现接口…

【尚硅谷】RocketMQ 消息队列学习笔记

RocketMQ 和 Kafka 消息队列概念比较&#xff1f; 好的&#xff01;RocketMQ 和 Kafka 都是分布式消息队列系统&#xff0c;它们的核心概念有很多相似之处&#xff0c;但在具体实现和命名上有所不同。下面我通过一个表格来对比 RocketMQ 和 Kafka 中的五个概念&#xff1a;消息…

操作系统 | 学习笔记 | 王道 | 4.1 文件系统基础

4.文件管理 4.1 文件系统基础 4.1.1 文件的基本概念 定义 文件是以计算机硬盘为载体的存储在计算机上的信息集合&#xff0c;在用户进行的输入、输出中&#xff0c;以文件位基本单位。 文件管理系统是实现的文件的访问、修改和保存&#xff0c;对文件维护管理的系统。 文件的…

2024重生之回溯数据结构与算法系列学习(11)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】

欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️ MYSQL REDIS Advance operation 专栏跑道二➡️ 24 Network Security -LJS ​ ​ ​ 专栏跑道三 ➡️HCIP&#xff1b;H3C-SE;CCIP——…

APP自动化搭建与应用

APP自动化环境搭建 用于做APP端UI自动化&#xff0c;adb连接手机设备。 需要的工具java编辑器&#xff1a;jdk、Android-sdk软件开发工具组、appium的python客户端、nodes.js、夜神模拟器、apk包、uiautomatorviewer 第一步&#xff1a;安装sdk&#xff0c;里面包含建立工具bu…

一、机器学习算法与实践_06迭代法和KMeans、线性回归、逻辑回归算法笔记

0 迭代法 迭代法不仅是机器学习、深度学习的核心&#xff0c;也是整个人工智能领域的重要概念&#xff0c;其对于算法的设计和实现至关重要 0.1 适合场景 对于不能一次搞定的问题&#xff0c;将其分成多步来解决&#xff0c;逐步逼近解决方案 0.2 典型应用 KMeans 聚类算法…

9-贪心算法

PDF文档下载&#xff1a;LeetCode-贪心算法-java 参考&#xff1a;代码随想录 题目分类大纲如下&#xff1a; 贪心算法理论基础 什么是贪心&#xff1f; 贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 贪心的套路&#xff08;什么时候用贪心&#xff…

【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)

第三部分&#xff1a;高级主题 第十章 高级函数与范式 在现代 JavaScript 开发中&#xff0c;高级函数与函数式编程范式正在逐渐成为开发者追求的目标。这种范式关注于函数的使用&#xff0c;消除副作用&#xff0c;提高代码的可读性和可维护性。 10.1. 高阶函数 高阶函数是…

【Python】文件及目录

文章目录 概要一、文件对象的函数1.1 open()函数1.2 文件对象的函数1.3 with语句 二、基于os和os.path模块的目录操作三、基于Pandas的文件处理3.1 Pandas读写各种类型文件 其他章节的内容 概要 本文主要将了打开文件的函数open()的参数&#xff0c;以及文件对象的函数&#x…

Vue2 + ElementUI + axios + VueRouter入门

之前没有pc端开发基础&#xff0c;工作需要使用若依框架进行了一年的前端开发.最近看到一个视频框架一步步集成&#xff0c;感觉颇受启发&#xff0c;在此记录一下学习心得。视频链接:vue2element ui 快速入门 环境搭建和依赖安装 安装nodejs安装Vue Cli使用vue create proje…

C/C++简单编译原理

我们写的头文件和.cpp文件究竟是如何在电脑中运行的&#xff1f; 先明确几个文件类型&#xff1a; 1、头文件&#xff08;.h .hpp&#xff09; 第三方头文件、系统头文件、自编头文件…… 2、编译单位&#xff08;.cpp .c cu&#xff09; 自己写的脚本文件 3、目标文件&…

JVM 系列知识体系全面回顾

经过几个月的努力&#xff0c;JVM 知识体系终于梳理完成了。 很早之前也和小伙伴们分享过 JVM 相关的技术知识&#xff0c;再次感谢大家支持和反馈。 最后再次献上 JVM系列文章合集索引&#xff0c;感兴趣的小伙伴可以点击查看。 JVM系列(一) -什么是虚拟机JVM系列(二) -类的…