Как вы пишете программу на C, чтобы увеличить число нажатием клавиши и автоматически уменьшать ее в секунду?

Я пытаюсь написать программу, в которой число начинается с 0, но когда вы нажимаете любую клавишу, она увеличивается на 1. Если ничего не нажимается, оно продолжает уменьшаться на 1 секунду, пока не достигнет 0. Каждое приращение или декремент отображается в окне консоли.

Проблема с моим подходом заключается в том, что ничего не происходит, пока я не getch() клавишу (то есть, она проверяет, нажата ли что-либо с помощью getch()). Как проверить, что ничего не нажата? И, конечно же, !getch() не работает, потому что для этого все равно нужно будет проверить keypress, который сводит на нет цель.

ОС: Windows 10 Enterprise, IDE: Код :: Блоки

void main()
{
    int i, counter = 0;
    for (i = 0; i < 1000; i++)
    {
        delay(1000);
        // if a key is pressed, increment it
        if (getch())
        {
            counter += 1;
            printf("\n%d", counter);
        }
        while (counter >= 1)
        {
            if (getch())
            {
                break;
            }
            else
            {
                delay(1000);
                counter--;
                printf("\n%d", counter);
            }
        }
    }
}

Ответ 1

Следующая короткая программа не требует ни ncurses, ни потоков. Однако для этого требуется изменить атрибуты терминала - используя tcsetattr(). Это будет работать в Linux и Unix-подобных системах, но не в Windows, которая не включает заголовочный файл termios.h. (Возможно, посмотрите этот пост, если вы заинтересованы в этой теме.)

#include <stdio.h>
#include <string.h>
#include <termios.h>

int main(int argc, char *argv[]) {
    struct termios orig_attr, new_attr;
    int c = '\0';
    // or int n = atoi(argv[1]);
    int n = 5;

    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    new_attr.c_cc[VMIN] = 0;
    // Wait up to 10 deciseconds (i.e. 1 second)
    new_attr.c_cc[VTIME] = 10; 
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    printf("Starting with n = %d\n", n);
    do {
        c = getchar();
        if (c != EOF) {
            n++;
            printf("Key pressed!\n");
            printf("n++ => %d\n", n);
        } else {
            n--;
            printf("n-- => %d\n", n);
            if (n == 0) {
                printf("Exiting ...\n");
                break;
            }
            if (feof(stdin)) {
                //puts("\t(clearing terminal error)");
                clearerr(stdin);
            }
        }
    } while (c != 'q');

    tcsetattr(fileno(stdin), TCSANOW, &orig_attr);

    return 0;
}

Жизненно важные моменты в том, что

new_attr.c_lflag &= ~(ICANON | ECHO);

выводит терминал из канонического режима (и отключает символ 'echo'),

new_attr.c_cc[VMIN] = 0;

помещает его в режим опроса (а не блокирования) и

new_attr.c_cc[VTIME] = 10;

инструктирует программу ждать ввода до 10 дсек.

Обновление (2019-01-13)

  • добавить clearerr(stdin) для очистки EOF на stdin (кажется необходимым на некоторых платформах)

Ответ 2

Это можно сделать с помощью многопоточности, как уже было предложено, но есть и другие возможности.

Например, ncurses имеет возможность ждать ввода с тайм-аутом.

Пример для ncurses (написанный Константином):

initscr();
timeout(1000);
char c = getch();
endwin();
printf("Char: %c\n", c);

Я думаю, что poll также может быть использован на stdin чтобы проверить, доступен ли вход.

И чтобы ваша программа была более отзывчивой, вы могли бы снизить свой сон или задержку до, например, 100 мс, и только уменьшить, если десять итераций сна прошли без ввода. Это уменьшит задержку ввода.

Ответ 3

Вот пример pthread, который работает на linux. Концепция в порядке, но для этого существуют, вероятно, существующие циклы/библиотеки.

#include <stdio.h>
#include<pthread.h>


void *timer(void* arg){
    int* counter = (int*)arg;
    while(*counter > 0){
        int a = *counter;
        printf("counter: %d \n", a);
        *counter = a - 1;
        sleep(1);
    }
}

int main(int arg_c, char** args){
    int i = 100;
    pthread_t loop;

    pthread_create(&loop, NULL, timer, &i);

    while(i>0){
        i++;
        getchar();
        printf("inc counter: %d \n", i);
    }
    printf("%d after\n", i);

    pthread_join(loop, NULL);

    return 0;
}

Это запускает второй поток, который имеет обратный отсчет. Это уменьшает счетчик каждую секунду. В основной нити он имеет петлю с getchar. Они оба изменяют i.

Ответ 4

Вам нужно использовать поток, и вам нужно использовать __sync_add_and_fetch и __sync_sub_and_fetch, чтобы избежать проблемы параллелизма

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <iostream>

static void* thread(void* p) {
    int* counter = (int*)p;
    while (1) {
        if (*counter > 0) {
            __sync_sub_and_fetch(counter, 1);
            printf("sub => %d\n", *counter);
        }  else {
            sleep(1);
        }
    }

    return NULL;
}

int main() {
    int counter = 0;
    char ch;

    struct termios orig_attr, new_attr;
    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    pthread_t pid;
    if (pthread_create(&pid, NULL, thread, &counter)) {
        fprintf(stderr, "Create thread failed");
        exit(1);
    }

    while(1) {
      char c = getchar();
      __sync_add_and_fetch(&counter, 1);
      printf("add: %d\n", counter);
    }

    return 0;
}

Ответ 5

Другой пример использования ncurses и таймеров POSIX и сигналов (и глобальных переменных).

#include <ncurses.h>
#include <signal.h>
#include <time.h>

int changed, value;

void timer(union sigval t) {
        (void)t; // suppress unused warning
        changed = 1;
        value--;
}

int main(void) {
        int ch;
        timer_t tid;
        struct itimerspec its = {0};
        struct sigevent se = {0};

        se.sigev_notify = SIGEV_THREAD;
        se.sigev_notify_function = timer;
        its.it_value.tv_sec = its.it_interval.tv_sec = 1;
        timer_create(CLOCK_REALTIME, &se, &tid);
        timer_settime(tid, 0, &its, NULL);

        initscr();
        halfdelay(1); // hit Ctrl-C to exit
        noecho();
        curs_set(0);

        for (;;) {
                ch = getch();
                if (ch != ERR) {
                        changed = 1;
                        value++;
                }
                if (changed) {
                        changed = 0;
                        mvprintw(0, 0, "%d ", value);
                        refresh();
                }
        }

        endwin();
}

Ответ 6

Вот еще один способ, который использует select для проверки наличия входа и ожидания. Нехорошее решение, но оно работает. Только Linux.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/select.h>

#define WAIT_TIME 1000 //Delay time in milliseconds

bool inputExists(void)
{
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0, &readfds);

    struct timeval tv;
    tv.tv_sec = tv.tv_usec = 0;

    if(select(1, &readfds, NULL, NULL, &tv))
        return true;
    else
        return false;
}

void wait()
{
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = WAIT_TIME * 1000;
    select(0, NULL, NULL, NULL, &tv);
}

int main(void)
{
    system("stty raw"); /* Switch to terminal raw input mode */

    unsigned int count = 0;
    for(;;)
    {
        if(inputExists())
        {
            char input[256] = {0};
            read(0, input, 255);
            count += strlen(input);

            printf("\rCount is now %d\n", count);
        }
        else if(count > 0)
        {
            count--;
            printf("\rDecremented count to %d\n", count);
        }

        puts("\rWaiting...");
        wait();
    }
}

Лучший способ, который позволяет избежать system("stty raw"), и те, \r будет использовать tcgetattr и tcsetattr:

struct termios orig_attr, new_attr;

tcgetattr(STDIN_FILENO, &orig_attr);
new_attr = orig_attr;
new_attr.c_lflag &= ~(ICANON | ECHO); //Disables echoing and canonical mode
tcsetattr(STDIN_FILENO, TCSANOW, &new_attr);

//...

tcsetattr(STDIN_FILENO, TCSANOW, &old_attr);

Ответ 7

Если вас не беспокоит мобильность, и вы всегда будете использовать Windows, вы можете использовать PeekConsoleInput, который сообщает вам, какие события ввода в консоль ждут.

Вы не можете (легко) использовать ReadConsoleInput, потому что он блокируется до тех пор, пока не будет хотя бы одно ожидающее событие ввода.

Ответ 8

У вашего кода две проблемы; один серьезный, другой нет.

Первая проблема, как вы выяснили, заключается в том, что getch() является блокирующей функцией. Другими словами, вызов функции не будет возвращен, пока не будет нажата клавиша.

Вторая проблема, хотя и незначительная, заключается в том, что программа реагирует только каждый раз на ввод.

Я немного изменил ваши требования, начиная исходный счетчик на 5.

#include <windows.h>

int main(void)
{
  int Counter;
  time_t StartTime;
  DWORD EventCount;


  Counter=5;
  do
  {
    StartTime=time(NULL);
    do
    {
      Sleep(10);  /* in ms. Don't hog the CPU(s). */
      GetNumberOfConsoleInputEvents(GetStdHandle(STD_INPUT_HANDLE),&EventCount);
    }
    while( (StartTime==time(NULL)) && (EventCount==0) );  
        /* Wait for a timeout or a key press. */

    if (EventCount!=0)  /* Have a key press. */
    {
      FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));  /* Clear the key press. */
      Counter++;
    }
    else  /* Timed out. */
      Counter--;

    printf("Counter = %d\n",Counter);
  }
  while(Counter>0);

  return(0);
}

Скомпилирован с использованием Microsoft Visual C++ 2015 (командная строка: "cl main.c").
Протестировано в Windows 7 и 10.