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…

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

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

Redis接口访问优化

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

操作系统 | 学习笔记 | 王道 | 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…

计算机网络——http和web

无状态服务器——不维护客户端 怎么变成有状态连接 所以此时本地建立代理—— 若本地缓存了——但是服务器变了——怎么办&#xff1f;

Pikachu-File Inclusion-远程文件包含

远程文件包含漏洞 是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的&#xff0c;因此漏洞一旦存在&#xff0c;危害性会很大。但远程文件包含漏洞的利用条件较为苛刻&#xff1b;因此&#xff0c;在web应用系统的功能设计上尽量不要让前端用户直接传变…

用java编写飞机大战

游戏界面使用JFrame和JPanel构建。背景图通过BG类绘制。英雄机和敌机在界面上显示并移动。子弹从英雄机发射并在屏幕上移动。游戏有四种状态&#xff1a;READY、RUNNING、PAUSE、GAMEOVER。状态通过鼠标点击进行切换&#xff1a;点击开始游戏&#xff08;从READY变为RUNNING&am…

CSS | 响应式布局之媒体查询(media-query)详解

media type(媒体类型)是CSS 2中的一个非常有用的属性&#xff0c;通过media type我们可以对不同的设备指定特定的样式&#xff0c;从而实现更丰富的界面。media query(媒体查询)是对media type的一种增强&#xff0c;是CSS 3的重要内容之一。随着移动互联网的发展&#xff0c;m…

reactNative本地调试localhost踩坑

本地调试请求localhost的时候 1.要和电脑处在同一局域网下面&#xff08;同一个wifi&#xff09; 2.把baseURL的localhost改成命令行中ipconfig查询到的IPv4 地址 . . . . . . . . . . . . : &#xff08;例如&#xff09;192.168.1.103 如果报错Net Work Error&#xff0c;可…

BMC pam认证的使用

1.说明 1.1 文档参考资料 https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_ADG.htmlhttp://www.fifi.org/doc/libpam-doc/html/pam_appl-3.htmlpdf文档: https://fossies.org/linux/Linux-PAM-docs/doc/adg/Linux-PAM_ADG.pdflinux-pam 中文文档pam 旧文p…

Redis基础二(spring整合redis)

Springboot整合Redis 一、Springboot整合redis ​ redis可以通过使用java代码来实现 第一部分文档中 在终端操作redis的所有命令&#xff0c;Spring已经帮我们封装了所有的操作&#xff0c;所以变得很简单了。 ​ Spring专门提供了一个模块来进行这些操作的封装&#xff0c;这…

【Linux】详解Linux下的工具(内含yum指令和vim指令)

文章目录 前言1. Linux下软件安装的方式2. yum2.1 软件下载的小知识2.2 在自己的Linux系统下验证yum源的存在2.3 利用yum指令下载软件2.4 拓展yum源&#xff08;针对于虚拟机用户&#xff09; 3. vim编辑器3.1 vim是什么&#xff1f;3.2 如何打开vim3.2 vim各模式下的讲解3.2.1…

Oracle中ADD_MONTHS()函数详解

文章目录 前言一、ADD_MONTHS()的语法二、主要用途三、测试用例总结 前言 在Oracle数据库中&#xff0c;ADD_MONTHS()函数用于在日期中添加指定的月数。 一、ADD_MONTHS()的语法 ADD_MONTHS(date, n) 其中&#xff0c;date是一个日期值&#xff0c;n是一个整数值&#xff0c…