ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

c++可视化操作(一)——用windows.h打造控制台点击型菜单与TaoToken配置

c++可视化操作(一)——用windows.h打造控制台点击型菜单与TaoToken配置 1. 从键盘输入到鼠标点击控制台菜单为什么值得折腾如果你写过计算器、校园卡管理系统或者象棋这类 C 大作业大概率经历过这样的场景菜单用cin choice让用户敲数字输错一个字符就死循环cin.clear()和cin.ignore()背得比乘法口诀还熟。界面全靠cout堆和----想加个高亮还得手动清屏重绘。程序逻辑本身没多复杂倒是输入校验和界面刷新吃掉了一半代码量。这篇要解决的就是这件事在 Windows 控制台里用windows.h做一套能用鼠标点击、能悬停高亮的菜单。用户把鼠标移到选项上那一行变色点一下直接进对应功能函数。不需要 Qt不需要 MFC一个.hpp加一个main.cpp就能跑。适合刚学完 C 基础、正在做大作业、想给控制台程序加点交互感的同学。核心思路其实不复杂。控制台本质上是一块字符缓冲区windows.h提供了ReadConsoleInput这个函数能把键盘和鼠标的原始事件读出来。鼠标事件里带着坐标COORD和按键状态dwButtonState我们只要在绘制菜单时把每个选项的坐标记下来之后拿点击坐标去比对命中就调用对应函数。悬停高亮同理鼠标移动事件里带当前坐标落在哪一行就给哪一行换颜色。另外做这类工具链开发时模型 API 的 Key 管理容易散落在各个脚本里。我习惯用 TaoToken 统一收口后面会给出具体的配置片段把 Base URL、Key、Model ID 三件套固定下来避免每个小工具都去翻环境变量。下面从窗口初始化开始一步步把可复制的代码搭出来。2. 前置准备控制台模式、句柄与 TaoToken 统一 Key 通道2.1 三个必须理解的概念句柄handleWindows 里操作任何内核对象都不是直接拿指针而是拿一个句柄。控制台有输入句柄和输出句柄分别用GetStdHandle(STD_INPUT_HANDLE)和GetStdHandle(STD_OUTPUT_HANDLE)获取。你可以把它理解成“操作凭证”后面所有读写都靠它。控制台console就是那个黑框窗口。它内部维护一块屏幕缓冲区记录每个字符格子的内容、颜色、光标位置。SetConsoleCursorPosition改光标SetConsoleTextAttribute改颜色GetConsoleScreenBufferInfo读当前状态。光标cursor不只是那个闪烁的竖线。它同时是“下一次输出会写到哪里”的标记。我们绘制菜单时正是靠“输出前先读光标坐标”来记录每个选项的位置。2.2 关闭快速编辑模式这一步是踩坑重灾区。Windows 控制台默认开启“快速编辑模式”你鼠标一拖就进入选择文本状态程序收不到鼠标事件点击判定直接失效。必须在程序启动时把它关掉void setmode() { DWORD mode; GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode); mode ~ENABLE_QUICK_EDIT_MODE; // 移除快速编辑模式 mode ~ENABLE_INSERT_MODE; // 移除插入模式 SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode); }注意ENABLE_MOUSE_INPUT不要移除否则鼠标事件根本不会进输入队列。2.3 TaoToken 配置把 Key 和 Base URL 固定下来写这类小工具时如果还要调模型做代码补全或文档生成Key 散在各处很麻烦。TaoToken 提供统一的 API 通道Base URL 固定为https://taotoken.net/apiKey 在控制台生成。我一般建一个settings.json放在项目根目录{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: claude-sonnet-4-20250514, timeout: 30 }如果你用 Cline 或 Claude Code 这类工具配置项名称可能不同但三件套不变Base URL 填https://taotoken.net/apiKey 填控制台生成的Model ID 按需选。Cline 的 MCP 配置里如果涉及本地服务记得把生产库连接串换成测试库别直连。Key 的获取入口在控制台的 API Keys 页面生成后复制一次就存好页面不再显示完整值。3. 可复制配置console.hpp 与 menu.hpp 完整代码3.1 console.hpp封装光标、颜色、鼠标事件这个头文件把底层 API 包一层上层菜单逻辑就不用反复写GetStdHandle了。#ifndef CONSOLE_HPP #define CONSOLE_HPP #include windows.h #include conio.h #include iostream #include iomanip using std::ios_base; using std::ostream; #define L_BUTTON 0x1 #define R_BUTTON 0x2 #define MOUSE_CLICK 0x0 #define MOUSE_MOVED 0x1 #define DOUBLE_CLICK 0x2 #define COLOR_default SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 0x07); #define COLOR_Black_Cyan SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 0x0b); #define COLOR_Yellow_Blue SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 0xe9); void setcursor(short x 0, short y 0) { COORD temp {x, y}; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), temp); } void setcursor(const COORD temp) { SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), temp); } void getcursor(COORD other) { CONSOLE_SCREEN_BUFFER_INFO temp; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), temp); other temp.dwCursorPosition; } COORD getcursor() { CONSOLE_SCREEN_BUFFER_INFO temp; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), temp); return temp.dwCursorPosition; } ostream operator(ostream pout, const COORD temp) { pout.setf(ios_base::fixed); pout [Cursor] X: std::setw(2) temp.X Y: std::setw(2) temp.Y; return pout; } void hidecursor(bool hide true) { CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), CursorInfo); CursorInfo.bVisible !hide; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), CursorInfo); } bool operator-(const COORD button, const COORD click) { if (button.Y click.Y button.X click.X click.X button.X 20) return true; else return false; } void setmode() { DWORD mode; GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode); mode ~ENABLE_QUICK_EDIT_MODE; mode ~ENABLE_INSERT_MODE; SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode); } MOUSE_EVENT_RECORD waitmouse(bool move true) { INPUT_RECORD record; DWORD reg; while (1) { Sleep(10); ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), record, 1, reg); if (record.EventType MOUSE_EVENT (move | record.Event.MouseEvent.dwEventFlags ! MOUSE_MOVED)) return record.Event.MouseEvent; } } void clean() { COORD temp getcursor(); setcursor(0, 0); for (int i 0; i temp.Y; i) std::cout \n; setcursor(0, 0); } void pause() { std::cout 请按任意键继续\n; std::cin.sync(); std::cin.get(); } #endif几个关键点。operator-重载用来判断点击是否落在某一行横向范围给了 20 个字符宽度你可以按菜单文字长度调整。waitmouse里Sleep(10)是防止 CPU 空转move参数控制是否过滤移动事件。clean用空格覆写而不是system(cls)因为cls会闪屏且重置光标。3.2 menu.hpp菜单类与点击分发#ifndef MENU_HPP #define MENU_HPP #include console.hpp #include vector using std::cout; using std::endl; using std::string; using std::vector; class Menu { private: struct node { COORD pos_; string display_; void (*pf_)(); node(string d Unkown., void (*p)() nullptr) : display_(d), pf_(p) {}; }; vectornode nodes_; string title_; protected: void recordposition() { for (auto p nodes_.begin(); p ! nodes_.end(); p) { getcursor(p-pos_); cout p-display_ \n\n; } } bool implement(COORD clickpos) { for (auto p nodes_.begin(); p ! nodes_.end(); p) { if (p-pos_ - clickpos) { hidecursor(0); clean(); p-pf_(); hidecursor(1); return true; } } return false; } void highlight(COORD hang) { clean(); cout title_ endl endl; for (auto p nodes_.begin(); p ! nodes_.end(); p) { if (p-pos_ - hang) { COLOR_Black_Cyan; cout p-display_ \n\n; COLOR_default; } else cout p-display_ \n\n; } } public: Menu() {}; ~Menu() {}; Menu settitle(string s) { title_ s; return *this; } Menu add(void (*p)() nullptr, string d Unkown.) { nodes_.push_back(node(d, p)); return *this; } void start() { setmode(); MOUSE_EVENT_RECORD mouse; hidecursor(1); clean(); cout title_ endl endl; recordposition(); do { Sleep(10); mouse waitmouse(); if (mouse.dwEventFlags MOUSE_MOVED) highlight(mouse.dwMousePosition); else if (mouse.dwButtonState L_BUTTON) implement(mouse.dwMousePosition); } while (mouse.dwButtonState ! R_BUTTON); Sleep(100); } }; #endifrecordposition在绘制前先读光标坐标存进node这是点击判定的基准。highlight每次重绘整个菜单命中行用青色背景。start的主循环里移动事件走高亮左键点击走分发右键退出。3.3 main.cpp 与编译命令#include menu.hpp #include iostream using std::cout; void show1() { cout choice1\n; } void show2() { cout choice2\n; } int main(void) { Menu loop; void (*p)(); p show1; loop.settitle(test).add(p, show1).add(show2, show2); loop.start(); return 0; }用 MSVC 编译cl /EHsc /std:c17 main.cpp /Fe:menu.exe用 MinGWg -stdc17 main.cpp -o menu.exe编译时确保console.hpp和menu.hpp与main.cpp同目录或者用-I指定路径。4. 验证请求编译运行与点击响应实测4.1 运行前检查清单编译通过后先别急着双击 exe。有几个点确认一下控制台窗口属性里“快速编辑模式”是否被程序关掉了运行后拖拽鼠标应该不会出现选择高亮窗口大小是否够放下菜单如果选项超过窗口高度recordposition记的坐标会跑到缓冲区外面点击判定就失效了。4.2 实测过程运行menu.exe屏幕输出test show1 show2把鼠标移到show1上那一行背景变青色移开恢复。点击show1清屏后输出choice1然后菜单重新绘制。点击show2同理。右键点击任意位置程序退出。我试过在highlight里把clean()去掉结果每次移动都叠加输出屏幕很快滚满。所以重绘前必须清屏这是控制台交互的基本节奏。4.3 坐标判定的边界operator-里横向范围是button.X click.X button.X 20。如果你的菜单文字超过 20 个字符点击后半段不会命中。改法很简单把 20 换成display_.length()但operator-是全局函数拿不到display_可以在node里存一个宽度字段或者把判定逻辑挪进Menu的成员函数里。纵向判定用的是button.Y click.Y只认单行。如果菜单项之间有空行鼠标落在空行上不会触发任何高亮这是符合预期的。5. 常见报错排查401、local proxy failed 与坐标偏移5.1 点击没反应鼠标拖拽出现选择框这是快速编辑模式没关掉。检查setmode()是否在start()最开始调用且ENABLE_QUICK_EDIT_MODE确实被清除了。有些 IDE 的内置终端会覆盖这个设置换成独立 cmd 或 Windows Terminal 再试。5.2 高亮错位鼠标在 A 行却高亮 B 行多半是recordposition记录坐标的时机不对。它必须在菜单文字输出之前读光标如果先cout再getcursor记的就是文字末尾的坐标。另外如果标题行数变了所有选项的 Y 坐标都会平移recordposition要跟着标题一起重算。5.3 调用模型接口时报 401如果你在工具链里用 TaoToken 调模型遇到 401 先查三件事Key 是否复制完整控制台只显示一次、Base URL 是否写成https://taotoken.net/api不要多加/v1或斜杠、请求头里Authorization是否是Bearer sk-xxx格式。三者任一不对都会 401。5.4 local proxy failed 或 reading choices 报错这类错误通常出现在 Cline、Claude Code 等工具的配置里。local proxy failed说明本地代理端口没起来或者被占用检查工具设置里的代理地址是否和实际监听端口一致。reading choices报错一般是返回体格式和客户端预期不匹配确认 Model ID 填的是服务端支持的模型别填一个不存在的名字。5.5 OAuth 相关报错Claude Code 走 OAuth 时如果报 token 过期重新走一遍授权流程即可。如果同时配了 API Key 和 OAuth注意优先级别让旧 token 覆盖了新 Key。5.6 Codex auth.json 配置要点用 Codex 类工具时auth.json里要写全三件套{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }少任何一个字段都可能连不上。改完记得重启工具进程有些客户端只在启动时读一次配置。6. 把菜单接进你的大作业下一步可以做什么这套点击菜单跑通之后最直接的用法是替换掉你大作业里的cin choice。把每个功能函数注册进Menu用户点一下就走对应逻辑输入校验的代码可以整段删掉。再进一步可以把highlight的颜色方案抽成配置不同菜单用不同配色。clean目前是全屏清如果菜单只占屏幕上半部分可以改成只清菜单区域减少闪烁。如果你想让菜单支持键盘方向键加回车waitmouse旁边再加一个waitkey读KEY_EVENT_RECORD就行结构是一样的。鼠标和键盘混用体验会更接近真正的 GUI。工具链这边把 TaoToken 的 Key 和 Base URL 固定到项目配置文件后后续加代码补全、文档生成、甚至让模型帮你写菜单项描述都不用再折腾鉴权。配置入口在控制台的 API Keys 页面模型对话可以直接在网页里试长期编码任务走 Coding Plan 更省心。接入文档里有各客户端的详细字段说明遇到字段对不上时翻一下比猜快。
返回列表