我正在致力于使用 WinAPI 创建一个类似于 Windows 版 Vim 的命令行界面 (CLI) 编辑器。目前,我专注于实现编辑模式(通过按“i”触发)和命令模式(通过按 Escape 激活)的逻辑,因此我知道解决此问题的两种方法:
1.) 第一种方法是创建一个 while 循环,连续侦听 Escape 键。该循环将在单独的线程上无限期地运行,以避免干扰程序流程。但这种方法的主要问题是这种方法可能会导致 CPU 消耗过高,减慢程序速度并可能导致与 CPU 相关的问题。
2.) 第二种方法涉及使用 Windows 挂钩。我编写了一个简单的程序来拦截键盘事件并检测“i”和 Escape 键。我已经实现了一个单独的变量来跟踪按键的来源。但是,我遇到了编辑器的问题,因为它似乎无法正常工作,并且无论我们在执行过程中按哪个键,光标都保持在之前的位置。有人可以帮我吗? 谢谢你。
这是相同的代码:-
HANDLE hConsole;
bool editMode = true;
void moveCursorToLastRow();
void moveCursorToFirstRow();
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0) {
KBDLLHOOKSTRUCT* kbStruct = (KBDLLHOOKSTRUCT*)lParam;
DWORD vkCode = kbStruct->vkCode;
if (vkCode == VK_ESCAPE && editMode == true) {
editMode = false;
moveCursorToLastRow();
}
else if (vkCode == 'I' && editMode == false) {
editMode = true;
moveCursorToFirstRow();
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
void moveCursorToFirstRow() {
// Set the cursor position to the first row
COORD cursorPosition = { 3, 2 };
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPosition);
}
void moveCursorToLastRow() {
// Get the console screen buffer size
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
// Set the cursor position to the last row
COORD cursorPosition = { 3, csbi.dwSize.Y - 1 };
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPosition);
}
int main(int argc, char* argv[]) {
HINSTANCE hInstance = GetModuleHandle(NULL);
HHOOK hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardHookProc, hInstance, 0);
if (hKeyboardHook == NULL) {
std::cerr << "Failed to install keyboard hook\n" << GetLastError();
return 1;
}
我尝试使用 Chatgpt 来解决这个问题,但它只会使代码变得更糟,并指示我实现一个循环,该循环将拦截来自钩子链的消息,尝试这样做,但陷入了无限循环,并且屏幕上没有显示任何内容尝试执行我的程序。
这是一些繁忙循环等待按键的示例代码。通常,这些类型的循环被认为是不好的,但我认为对于这种类型的应用程序来说是可以的。
ReadConsoleInput
会阻塞,直到读取输入记录,因此它不会不断循环。
注意:我刚刚写了这篇文章,所以它还远未完成。另外,我从来没有写过这样的程序,所以可能有更好的方法。
#include <Windows.h>
#include <string>
// Reads a key from the keyboard. Blocks until user enters a key.
KEY_EVENT_RECORD getchar(HANDLE con)
{
DWORD number_of_events;
INPUT_RECORD record;
while (true)
{
ReadConsoleInput(con, &record, 1, &number_of_events);
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown)
{
return record.Event.KeyEvent;
}
}
}
// Print to console
void echo(char c, COORD& pos, HANDLE con)
{
SetConsoleCursorPosition(con, pos);
WriteConsole(con, &c, 1, nullptr, nullptr);
pos.X += 1;
}
void echo(const std::string& msg, COORD& pos, HANDLE con)
{
SetConsoleCursorPosition(con, pos);
WriteConsole(con, msg.c_str(), msg.length(), nullptr, nullptr);
pos.X += msg.length();
}
int main()
{
HANDLE con_in = GetStdHandle(STD_INPUT_HANDLE);
HANDLE con_out = GetStdHandle(STD_OUTPUT_HANDLE);
if (con_in == INVALID_HANDLE_VALUE || con_out == INVALID_HANDLE_VALUE) return -1;
COORD pos{ 0 }; // Position to type next char
COORD cmd_pos{ 0 }; // Position to type next command input char
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
cmd_pos.Y = csbi.dwSize.Y - 1;
// Cursor position. It can be either pos or cmd_pos.
COORD *current = &pos;
while (true)
{
KEY_EVENT_RECORD key = getchar(con_in);
// Escape key? Enter command mode
if (key.wVirtualKeyCode == 27) {
current = &cmd_pos;
echo("COMMAND: ", *current, con_out);
}
// New line
else if (key.wVirtualKeyCode == 13) {
// In text entry mode?
if (current == &pos) {
pos.X = 0;
pos.Y += 1;
}
// In command mode?
else {
current = &pos;
cmd_pos.X = 0;
// TODO: Clear the command....
// TODO: Execute command.....
}
SetConsoleCursorPosition(con_out, *current);
}
else {
echo(key.uChar.AsciiChar, *current, con_out);
}
}
return 0;
}
另一种选择是将
getchar
函数放在单独的线程中,并使用事件来提醒主程序有可用的输入。