C++ Interrupt or Cancel getch();

3.7k views Asked by At

I have a simple timer. It's in a function running in a thread separate from the main. Using std::future, the function returns a simple bool that says whether the timer has hit a specific number or not.

I am using getch(); to see if the user pressed a letter key.

If the timer returns true, that it hit a designated number, I need to cancel getch(); and move to the next step in the code. Moving to the next step is easy.

It's been 2 weeks and I can not find a solution to my problem.

The Problem: How on earth, can I interrupt or cancel a call to getch();? Is this even possible?

I'm using getch(); to identify which letter keys were pressed.

C++11 Visual Studio.

3

There are 3 answers

0
wally On

The operating system must provide access to the keyboard. So on Windows for example, the best is probably to deal with input on the operating system's terms as described here.

With the standard c++ library functions one can read characters from the std::cin stream. The problem is that those characters are only passed from the operating system after the user presses Enter (which also adds a newline \n character).

If you can tolerate the need to press the return key after a character is typed then the following could work. This program executes get() in a separate thread so that it doesn't block the program if no key is pressed or if Enter is not pressed and only uses standard c++11. This program will however not complete (i.e. join the thread) unless the user types q or sends the EOF.

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::condition_variable cv{};
std::mutex mtx;
std::queue<char> char_queue{};
bool quit{false};

void add_chars_to_queue()
{
    char c{};
    for(;;) {
        c = static_cast<char>(std::cin.get());
        if(!std::cin) {
            std::unique_lock<std::mutex> lck{mtx};
            quit = true;
            cv.notify_all();
            return;
        }
        if(c == 'q' || c == 'Q') {
            std::unique_lock<std::mutex> lck{mtx};
            quit = true;
            char_queue.push(c);
            cv.notify_all();
            return;
        }
        if(c == '\n')
            continue;
        std::unique_lock<std::mutex> lck{mtx};
        char_queue.push(c);
        cv.notify_all();
    }
}


std::string get_key_or_wait(std::chrono::system_clock::duration d)
{
    std::unique_lock<std::mutex> lck{mtx};
    for(int i{10}; i > 0; --i) {
        cv.wait_for(lck, d / 10., []() {return quit || !char_queue.empty(); });
        if(!char_queue.empty())
            break;
        if(quit)
            return{"Quitting.\n"};
        std::cout << "Countdown at " << i << '\n';
    }
    std::string return_string{};
    if(!char_queue.empty()) {
        return_string += "Obtained a character from the stream before the timer ran out. Character was: ";
        return_string += char_queue.front();
        char_queue.pop();
    }
    else {
        return_string = "Timer ran out.";
    }

    return return_string;
}

int main()
{
    std::thread get_chars{[]() {add_chars_to_queue(); }};

    std::cout << "Type q to exit.\n";
    for(int i{}; i < 3; ++i) {
        {
            std::lock_guard<std::mutex> lck{mtx};
            if(quit)
                break;
        }
        std::cout << "Waiting for key press followed by <enter>.\n"; 
        std::cout << get_key_or_wait(std::chrono::seconds(10)) << '\n';
    }

    get_chars.join();
    return 0;
}

Output:

Type q to exit.
Waiting for key press followed by <enter>.
Countdown at 10
Countdown at 9
Countdown at 8
a
Obtained a character from the stream before the timer ran out. Character was: a
Waiting for key press followed by <enter>.
Countdown at 10
Countdown at 9
Countdown at 8
Countdown at 7
Countdown at 6
Countdown at 5
Countdown at 4
Countdown at 3
Countdown at 2
Countdown at 1
Timer ran out.
Waiting for key press followed by <enter>.
Countdown at 10
Countdown at 9
Countdown at 8
bCountdown at 7
Countdown at 6
Countdown at 5

Obtained a character from the stream before the timer ran out. Character was: b
q
0
Tyler Lewis On

As others have mentioned, getch() is platform specific. This would be a short example to do what you want to do. The basic idea is to run a non-blocking getch() in an event loop in a separate thread, and exit the event loop via a bool flag when the time limit is up.

#include <iostream>
#include <thread>
#include <chrono>
#include <future>
#include <conio.h>
#include <Windows.h>


int nonBlockingGetChar();
int nonBlockingGetCharTask();

//This should be atomic. but I'm skipping it right here'
static bool getCharAlive{ false };

int main()
{
    //Timeout
    static const long long TIMEOUT{ 1000 * 5 };

    auto startTime = std::chrono::high_resolution_clock::now();
    auto endTime = std::chrono::high_resolution_clock::now();
    long long elapsedMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
    std::future<int> getCharHandle{ std::async(std::launch::async, nonBlockingGetCharTask) };
    do {
        //Other code here
        endTime = std::chrono::high_resolution_clock::now();
        elapsedMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
        if (elapsedMilliseconds >= TIMEOUT) {
            //If the timer hit a certain amount, cancel the getChar task
            getCharAlive = false;
            while (getCharHandle.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
                //Wait for getCharAlive to exit
            }
            std::cout << "User did not enter anything in the alotted time" << std::endl;
            break; //Move on to next step
        } else {
            //Otherwise, check if the getCharTask returned anything
            if (getCharHandle.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
                int userInput{ getCharHandle.get() };
                if (userInput == -1) {
                    std::cout << "User did not enter anything in the alotted time" << std::endl;
                } else {
                    std::cout << "User entered keycode " << userInput << std::endl;
                    //Do whatever with the user input
                }
                break; //Move on to next step
            }
        }
    } while (true);

    //And so on to step 2
}

int nonBlockingGetChar()
{
    if (_kbhit()) {
        return _getch();
    } else {
        return -1;
    }
}

int nonBlockingGetCharTask()
{
    getCharAlive = true;
    do {
        int returnValue{ nonBlockingGetChar() };
        if (returnValue != -1) {
            return returnValue;
        }
    } while (getCharAlive);
    return -1;
}
2
Mikel F On

This code will allow you to do what you want, but it does not take advantage of newer language features, nor is it portable.

events[0] = CreateEvent(NULL,FALSE,FALSE,NULL); // Obtain a Windows handle to use with a timer
events[1] = GetStdHandle(STD_INPUT_HANDLE); // Get a Windows handle to the keyboard input

    // Create a timer object that will strobe an event every ten seconds 
    DemoTimer = timeSetEvent(10000,0,(LPTIMECALLBACK)events[0],NULL,TIME_PERIODIC|TIME_CALLBACK_EVENT_SET); 
    while (done == false)
    {
        // Wait for either the timer to expire or a key press event
        dwResult = WaitForMultipleObjects(2,events,false,INFINITE);

        if (dwResult == WAIT_FAILED)
        {
            dwResult = GetLastError();
            done = true;
        }
        else
        {
        if (dwResult == WAIT_OBJECT_0) // WAIT_OBJECT_0 corresponds to the timer event
            {
                DoTimeoutEvent();
            }
            else
            {            
                   // Any other event will be a keypress

                    if (_kbhit() != 0) // Verify that a key was pressed so that we do not block when we query for a value
                    {
                        int val = _getch();
                        // At this point, we process the key value
                    }
                }
            }
        }

You are not going to be able to break out of getch(). The best bet is to check for data in the STDIN buffer and only make the call after you have something to read. This example uses kbhit(), but instead of using a polling loop where it periodically checks for buffer activity, it hooks the underlying handle to the input stream and waits for activity.

Using a second thread as a one-shot timer is also not the most efficient way to go. The timer in this code uses a Microsoft specific object. It is coded to fire off every ten seconds, but you can certainly change that.