0

I have a loop running that displays data read from a serial port on a console screen, the same screen lists some options for the user, such as the option to enter a file name that data would be logged to, or exit. The user presses the required key to enter the desired option.

After the key press and the option function entered any key presses can be ignored/discarded but I am not able to find a way to discard the key presses/ clear the cin buffer resulting in the key press being shown on the console.

Is there any way to clear the cin buffer without the user having to take any action? Or possibly some better way to provide the same functionality?

A stripped down version of code here: (this should display the local time while giving the user the option to enter a "file name" or exit, no serial port stuff included)

#include <iostream>
#include <limits>
#include <chrono>
#include <windows.h>              
using namespace std;

// global variables
char SavedFileName[20];
HANDLE hConsole;
CONSOLE_SCREEN_BUFFER_INFO csbi;


// functions
void GetTime(void){
    char buffer [20];
    time_t now = time(NULL);
    strftime(buffer , 20, "%H:%M:%S %d/%m/%Y", localtime(&now));
    cout << "local time: " << buffer << endl;
}
bool is1keypressed(void){
    if(GetAsyncKeyState(49) & 0x8000){
        return true;
    }
    return false;
}
bool is9keypressed(void){
    if(GetAsyncKeyState(57) & 0x8000){
        return true;
    }
    return false;
}
int ClearConsole(void){
    hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    SMALL_RECT scrollRect;
    COORD scrollTarget;
    CHAR_INFO fill;
    
    // Get the number of character cells in the current buffer.
    if (!GetConsoleScreenBufferInfo(hConsole, &csbi))
        return 0;
  

    // Scroll the rectangle of the entire buffer.
    scrollRect.Left = 0;
    scrollRect.Top = 0;
    scrollRect.Right = csbi.dwSize.X;
    scrollRect.Bottom = csbi.dwSize.Y;

    // Scroll it upwards off the top of the buffer with a magnitude of the entire height.
    scrollTarget.X = 0;
    scrollTarget.Y = (SHORT)(0 - csbi.dwSize.Y);

    // Fill with empty spaces with the buffer's default text attribute.
    fill.Char.UnicodeChar = TEXT(' ');
    fill.Attributes = csbi.wAttributes;

    // Do the scroll
    ScrollConsoleScreenBuffer(hConsole, &scrollRect, NULL, scrollTarget, &fill);
    
    return 0;
}

int main(){
    bool exit = false;
    
    // clear the console
    ClearConsole();
    
    // main loop
    while(!exit){
        
        // Move the cursor to the top left corner too.
        csbi.dwCursorPosition.X = 0;
        csbi.dwCursorPosition.Y = 0;
        SetConsoleCursorPosition(hConsole, csbi.dwCursorPosition);
        
        // prints the local time each on iteration of loop
        GetTime();
        
        // user options
        cout << "\npress (1) enter file name - saved file name is: " << SavedFileName;
        cout << "\npress (9) to exit." << endl;
        
        // test user options
        if(is1keypressed()){  
                        cout << "option 1 - press ENTER to proceed " << endl;
            cin.clear();
            cin.ignore(numeric_limits<streamsize>::max(), '\n');
        
            bool validentry = false;
            while(!validentry){
                cout << "Enter a file name, max 19 characters: ";
                cin.getline(SavedFileName, 20, '\n');
                
                if(cin.fail()){
                    validentry = false;
                    cout << endl << "File name is too large, enter 19 characters or less. " << endl;
                    cin.clear();
                    cin.ignore(numeric_limits<streamsize>::max(), '\n');
                }else
                    validentry = true;
                
            }
            ClearConsole();
        }
        
        exit = is9keypressed();
    }
    cout << "\n end program" << endl;
    return 0;
}

I have experimented with cin.ignore, this leaves the user to press Enter to proceed. Without cin.ignore the user has to delete the "option" characters themselves.

Gusman
  • 3
  • 2
  • There is no good, portable way. Hopefully one of the folks here knows some good Windows-specific tricks. – user4581301 Jan 20 '23 at 15:32
  • @user4581301 thanks, that makes me feel slightly better knowing that I wasn't missing something obvious! I feel an overly complicated work around coming on... – Gusman Jan 20 '23 at 15:39
  • In general iostreams are pretty simple. They have to work the same on a 8-bit microcontroller that bit-bangs data out a two-wire serial port as they do on a desktop PC with a full-featured terminal, so you can't add anything to the PC you can't do on said bit-banger. The best option may be to use Windows API calls. – user4581301 Jan 20 '23 at 15:45
  • 1
    Just use library ncurses which will detach you from standard input. Standard input is an abstraction for multiple devices: user keyboard, files, network connection, character scanner, .... . This is the reason it works like that. By using ncurses you will always use a keyboard and you will gain more control how it works. – Marek R Jan 20 '23 at 15:58
  • If you use [`ReadConsole`](https://learn.microsoft.com/en-us/windows/console/readconsole) instead of `std::cin`, then you can control whether the input is echoed to the screen by using [`SetConsoleMode`](https://learn.microsoft.com/en-us/windows/console/setconsolemode). If you want even more control over everything, you can use the function [`ReadConsoleInput`](https://learn.microsoft.com/en-us/windows/console/readconsoleinput) instead. – Andreas Wenzel Jan 20 '23 at 18:21
  • Your `is*keypressed()` functions can be reduced down to `return (GetAsyncKeyState() & 0x8000);` – Thomas Matthews Jan 20 '23 at 18:39

1 Answers1

0

Okay, Windows console stuff. Only works on Windows.

Playing with this stuff is never as easy as advertised, but for what you want to do, we can make some pretty stripped-down Windows-only code. (If, at some future point, you wish to port to Linux, the design here can be pretty easily swapped-out for similar code on Linux.)


Minimal Windows Console Functions

#include <ciso646>
#include <utility>
#include <windows.h>

namespace console
{
  inline auto hStdIn () { return GetStdHandle( STD_INPUT_HANDLE ); }
  inline auto hStdOut() { return GetStdHandle( STD_OUTPUT_HANDLE ); }

  auto CSBI()
  {
    CONSOLE_SCREEN_BUFFER_INFO _csbi;
    GetConsoleScreenBufferInfo( hStdOut(), &_csbi );
    return _csbi;
  }

  void clear_screen()
  // Function
  //   Clears the screen to blanks using the current text color and puts the
  //   cursor in the upper-left cell.
  {
    DWORD _count;
    DWORD _cell_count;
    COORD _home_coords = { 0, 0 };
    auto  _csbi        = CSBI();
    _cell_count = _csbi.dwSize.X * _csbi.dwSize.Y;
    if (!FillConsoleOutputCharacter( hStdOut(), (TCHAR)' ',        _cell_count, _home_coords, &_count )) return;
    if (!FillConsoleOutputAttribute( hStdOut(), _csbi.wAttributes, _cell_count, _home_coords, &_count )) return;
    SetConsoleCursorPosition( hStdOut(), _home_coords );
  }

  void goto_xy( int x, int y )
  // Function
  //   Position the text cursor on the screen.
  //
  // Arguments
  //   x - column coordinate, starting at zero from the left.
  //   y - row coordinate, starting at zero from the top.
  {
    COORD _coords = { (SHORT)x, (SHORT)y };
    SetConsoleCursorPosition( hStdOut(), _coords );
  }
  
  bool is_key_pressed( int timeout_ms = 0 )
  // Function
  //   Wait for key input to become available.
  //
  // Argument
  //   The maxiumum amount of time to wait, expressed in milliseconds.
  //   May also be one of the following:
  //     0 - return immediately.
  //    -1 - wait indefinitely.
  //
  // Returns
  //   true  : If key input is available
  //   false : If not
  {
    auto is_key_event_waiting = []()
    {
      DWORD        _n;
      INPUT_RECORD _rec;
      while (PeekConsoleInputW( hStdIn(), &_rec, 1, &_n) and _n)
      {
        if (_rec.EventType != KEY_EVENT) continue;
        if (_rec.Event.KeyEvent.bKeyDown) return true;
        if ((_rec.Event.KeyEvent.wVirtualKeyCode == VK_MENU) and _rec.Event.KeyEvent.uChar.UnicodeChar)
          return true;
        ReadConsoleInputW( hStdIn(), &_rec, 1, &_n );
      }
      return false;
    };
    
    // Get rid of all unwanted events
    if (is_key_event_waiting()) return true;
    
    // Else wait until event available or timeout
    if (WaitForSingleObject( hStdIn(), (DWORD)timeout_ms ) != WAIT_OBJECT_0) return false;
    
    // We only want key events
    return is_key_event_waiting();
  }

  char32_t read_key()
  // Function
  //   Wait for and return the next key input available.
  //
  // Returns
  //   0                  : next read_key() returns a Windows Virtual Key Code, like `VK_F1` or `VK_UP`
  //   Unicode code point : any normal input key
  {
    static char32_t _vkey = 0;
    if (_vkey) return std::exchange( _vkey, 0 );

    DWORD        _n;
    INPUT_RECORD _rec;
    while (true)
    {
      ReadConsoleInputW( hStdIn(), &_rec, 1, &_n );
      if (_rec.EventType == KEY_EVENT)
      {
        // Key release events for the ALT/MENU key where UnicodeChar != 0
        // means an Alt-code was entered on the numeric keypad. We'll report that.
        // All other key release events are ignored.
        if (!_rec.Event.KeyEvent.bKeyDown)
          if ((_rec.Event.KeyEvent.wVirtualKeyCode != VK_MENU) or !_rec.Event.KeyEvent.uChar.UnicodeChar)
            continue;

        // Normal Unicode code point
        if (_rec.Event.KeyEvent.uChar.UnicodeChar)
          return _rec.Event.KeyEvent.uChar.UnicodeChar;

        // Special keys and function keys
        _vkey = _rec.Event.KeyEvent.wVirtualKeyCode;
        return 0;
      }
    }
  }
} // namespace console

Notes:

  • It works on both the Windows Terminal and on both current and past versions of the Windows Console.

  • There is no initialization or finalization necessary to use this particular magic.

  • Assumes a human is present. More robust code would check that the standard streams are attached to a console/terminal at initialization and complain (and quit) if not.

  • Only one thread should ever be messing with the console, so if you are doing anything multi-threaded, only console:: from one of them, ever. (This is pretty much true of any user-application UI.)


Your program, updated

#include <ctime>
#include <iostream>
#include <string>


std::string SavedFileName;


void DrawMenu(){
    console::clear_screen();
    console::goto_xy(0, 1);
    std::cout << "\npress (1) enter file name - saved file name is: " << SavedFileName;
    std::cout << "\npress (9) to exit.\n";
}

void DrawTime(){
    char buffer [20];
    time_t now = time(NULL);
    strftime(buffer , 20, "%H:%M:%S %d/%m/%Y", localtime(&now));
    console::goto_xy(0, 0);
    std::cout << "local time: " << buffer << " ";
}

int main(){
    DrawMenu();

    bool done = false;
    while(!done){

        DrawTime();

        if (console::is_key_pressed(500))
            switch (console::read_key()){

                case '1':
                    console::goto_xy(0, 5);
                    std::cout << "option 1\nEnter a file name: ";
                    getline(std::cin, SavedFileName);
                    DrawMenu();
                    break;

                case '9':
                    console::goto_xy(0, 5);
                    std::cout << "option 2\nReally quit (y/[n])? ";
                    {
                      auto c = console::read_key();
                      if (!c) console::read_key();
                      else done = (c == 'y') or (c == 'Y');
                    }
                    if (!done) DrawMenu();
                    break;
                    
                case 0:
                    // Ignore special and function keys
                    console::read_key();
                    break;
            }
    }
    std::cout << "\n\nGood-bye\n";
}

Notes:

  • I tend to alphabetize #includes, which makes it easier when the list gets long. Also, <chrono> is for time-based C++ functions, but you use the C date-time stuff from <ctime>. Be careful to include the correct headers.

  • It is a good idea to get used to writing std:: in front of everything. It is generally a bad idea to simply dump the entire Standard Library into your program’s namespace[citation needed].

  • Strings in C++ are, in general, best handled with the std::string class. Please use it. It’ll make your life infinitely easier.

  • The Draw*() functions are carefully organized to work together. This requires you to have a good idea of how your output is going to look.

    • DrawMenu() clears the screen and draws only the menu options
    • DrawTime() only draws the time
  • We do not clear the display every frame. Clearing the console is an expensive operation, often producing a noticeable blink. Avoid it if possible. In our case, we only want to update the time every frame, and redraw everything only after we have finished with some user interaction.

  • The frame rate is set by how long we are willing to wait for input to be available. In this example I choose 500 ms, which is a reasonably snappy response time. Avoid going below 150–250 ms, as that bogs the processor down with your wait loop. (This is why we don’t use _kbhit() from <conio.h>.)

  • Be careful with names. The way you name things makes a difference.

    • DrawTime() is a better name than GetTime() because it better informs you what it is doing — drawing the time for the user. It doesn’t actually get (and return) the time (to the caller).
      One way to think about it is that the fact that getting the time is a subtask of showing the time.
    • done is a better name than exit because it is a nominative state, not an action. (And also because it does not clash with the Standard Library exit() function name.)
    • The general rule of thumb is to use nouns for objects and verbs for functions.
  • Commentary should only exist to clarify code, not to describe it. For example, commentary in the console:: namespace describes how user functions are to be used. The only commentary in the main program reminds us why we are calling console::get_key() again, which is otherwise non-obvious.


Compiling and running the code is as easy as concatenating the two code-blocks together and running it through the compiler.

  • MSVC
    cl /EHsc /W4 /Ox /std=c++17 example.cpp /D_CRT_SECURE_NO_WARNINGS
    
  • Clang (or MinGW)
    clang++ -Wall -Wextra -Werror -pedantic-errors -O3 -std=c++17 example.cpp -o example.exe -D_CRT_SECURE_NO_WARNINGS
    
Dúthomhas
  • 8,200
  • 2
  • 17
  • 39
  • many thanks for this detailed answer and the notes, these are very welcome. Your code preforms exactly as desired, I'm now going to study it to ensure I understand how it works. – Gusman Jan 23 '23 at 11:20