Đầu vào bàn phím không chặn C


82

Tôi đang cố gắng viết một chương trình bằng C (trên Linux) lặp lại cho đến khi người dùng nhấn một phím, nhưng không yêu cầu nhấn phím để tiếp tục mỗi vòng lặp.

Có một cách đơn giản để làm điều này? Tôi nghĩ mình có thể làm được select()nhưng điều đó có vẻ như rất nhiều việc.

Ngoài ra, có cách nào để bắt một phím ctrl- cđể thực hiện dọn dẹp trước khi chương trình đóng thay vì io không chặn không?


những gì về mở một chủ đề?
Nadav B

Câu trả lời:


65

Như đã nêu, bạn có thể sử dụng sigactionđể bẫy ctrl-c hoặc selectđể bẫy bất kỳ đầu vào chuẩn nào.

Tuy nhiên, lưu ý rằng với phương pháp thứ hai, bạn cũng cần đặt TTY để nó ở chế độ ký tự tại một thời điểm thay vì chế độ dòng tại một thời điểm. Cái sau là mặc định - nếu bạn nhập một dòng văn bản, nó sẽ không được gửi đến stdin của chương trình đang chạy cho đến khi bạn nhấn enter.

Bạn cần sử dụng tcsetattr()chức năng để tắt chế độ ICANON và có thể tắt cả ECHO. Từ bộ nhớ, bạn cũng phải đặt thiết bị đầu cuối trở lại chế độ ICANON khi chương trình thoát ra!

Chỉ để hoàn thiện, đây là một số mã tôi vừa gõ lên (nb: không kiểm tra lỗi!) Để thiết lập Unix TTY và mô phỏng các <conio.h>chức năng DOS kbhit()getch():

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

struct termios orig_termios;

void reset_terminal_mode()
{
    tcsetattr(0, TCSANOW, &orig_termios);
}

void set_conio_terminal_mode()
{
    struct termios new_termios;

    /* take two copies - one for now, one for later */
    tcgetattr(0, &orig_termios);
    memcpy(&new_termios, &orig_termios, sizeof(new_termios));

    /* register cleanup handler, and set the new terminal mode */
    atexit(reset_terminal_mode);
    cfmakeraw(&new_termios);
    tcsetattr(0, TCSANOW, &new_termios);
}

int kbhit()
{
    struct timeval tv = { 0L, 0L };
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(0, &fds);
    return select(1, &fds, NULL, NULL, &tv);
}

int getch()
{
    int r;
    unsigned char c;
    if ((r = read(0, &c, sizeof(c))) < 0) {
        return r;
    } else {
        return c;
    }
}

int main(int argc, char *argv[])
{
    set_conio_terminal_mode();

    while (!kbhit()) {
        /* do some work */
    }
    (void)getch(); /* consume the character */
}

1
Getch () có phải trả về phím nào đã được nhấn hay chỉ đơn giản là dùng ký tự?
Salepate

@Salepate nó phải trả về ký tự. Đó là (void)bit nhận được nhân vật đó và sau đó ném nó đi.
Alnitak

1
ồ tôi hiểu rồi, có thể tôi đang làm sai nhưng khi tôi sử dụng getch () trên printf ("% c"); nó hiển thị các ký tự kỳ lạ trên màn hình.
Salepate

Nhẹ chỉnh sửa, bạn cần phải #include <unistd.h> cho read () để làm việc
jernkuan

Có cách nào đơn giản để đọc toàn bộ dòng (ví dụ: với 'getline') thay vì một ký tự không?
azmeuk

15

select()là một mức quá thấp để thuận tiện. Tôi khuyên bạn nên sử dụng ncursesthư viện để đặt thiết bị đầu cuối ở chế độ bẻ khóa và chế độ trì hoãn, sau đó gọi getch(), sẽ trả về ERRnếu không có ký tự nào sẵn sàng:

WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);

Tại thời điểm đó, bạn có thể gọi getchmà không bị chặn.


Tôi cũng đã nghĩ đến việc sử dụng các lời nguyền, nhưng vấn đề tiềm ẩn với đó là lệnh gọi initscr () xóa màn hình và nó cũng có thể cản trở việc sử dụng stdio bình thường cho đầu ra màn hình.
Alnitak

5
Vâng, những lời nguyền là khá tất cả hoặc không có gì.
Norman Ramsey

11

Trên hệ thống UNIX, bạn có thể sử dụng sigactionlệnh gọi để đăng ký bộ xử lý SIGINTtín hiệu cho tín hiệu đại diện cho chuỗi phím Control + C. Bộ xử lý tín hiệu có thể đặt một cờ sẽ được kiểm tra trong vòng lặp làm cho nó ngắt một cách thích hợp.


Tôi nghĩ có lẽ tôi sẽ làm điều này cho dễ dàng, nhưng tôi sẽ chấp nhận câu trả lời khác vì nó gần với những gì tiêu đề yêu cầu hơn.
Zxaos

6

Bạn có thể muốn kbhit();

//Example will loop until a key is pressed
#include <conio.h>
#include <iostream>

using namespace std;

int main()
{
    while(1)
    {
        if(kbhit())
        {
            break;
        }
    }
}

điều này có thể không hoạt động trên tất cả các môi trường. Một cách di động sẽ là tạo một chuỗi giám sát và đặt một số cờ trêngetch();


4
chắc chắn không phải hàng xách tay. kbhit () là một hàm IO của bảng điều khiển DOS / Windows.
Alnitak

1
Nó vẫn là một câu trả lời hợp lệ nhiều công dụng. OP không chỉ định tính di động hoặc bất kỳ nền tảng cụ thể nào.
AShelly

3
Alnitak: Tôi không biết nó ngụ ý một nền tảng - thuật ngữ Windows tương đương là gì?
Zxaos

2
@Alnitak tại sao "không chặn", như một khái niệm lập trình, lại quen thuộc hơn trong môi trường Unix?
MestreLion

4
@Alnitak: Ý tôi là tôi đã quen với thuật ngữ "cuộc gọi không chặn" từ rất lâu trước khi chạm vào bất kỳ * nix nào .. vì vậy, giống như Zxaos, tôi sẽ không bao giờ nhận ra nó có nghĩa là một nền tảng.
MestreLion

4

Một cách khác để nhận được tính năng nhập bằng bàn phím không chặn là mở tệp thiết bị và đọc nó!

Bạn phải biết tệp thiết bị bạn đang tìm kiếm, một trong số / dev / input / event *. Bạn có thể chạy cat / proc / bus / input / devices để tìm thiết bị bạn muốn.

Mã này phù hợp với tôi (chạy với tư cách quản trị viên).

  #include <stdlib.h>
  #include <stdio.h>
  #include <unistd.h>
  #include <fcntl.h>
  #include <errno.h>
  #include <linux/input.h>

  int main(int argc, char** argv)
  {
      int fd, bytes;
      struct input_event data;

      const char *pDevice = "/dev/input/event2";

      // Open Keyboard
      fd = open(pDevice, O_RDONLY | O_NONBLOCK);
      if(fd == -1)
      {
          printf("ERROR Opening %s\n", pDevice);
          return -1;
      }

      while(1)
      {
          // Read Keyboard Data
          bytes = read(fd, &data, sizeof(data));
          if(bytes > 0)
          {
              printf("Keypress value=%x, type=%x, code=%x\n", data.value, data.type, data.code);
          }
          else
          {
              // Nothing read
              sleep(1);
          }
      }

      return 0;
   }

ví dụ tuyệt vời, nhưng tôi gặp phải vấn đề trong đó Xorg sẽ tiếp tục nhận các sự kiện quan trọng, khi thiết bị sự kiện ngừng phát ra khi hơn sáu phím được giữ: \ lạ đúng không?
ThorSummoner

3

Thư viện lời nguyền có thể được sử dụng cho mục đích này. Tất nhiên, select()và các bộ xử lý tín hiệu cũng có thể được sử dụng ở một mức độ nhất định.


1
nguyền rủa là cả tên của thư viện và những gì bạn sẽ lẩm bẩm khi sử dụng nó;) Tuy nhiên, vì tôi sẽ đề cập đến nó +1 với bạn.
Wayne Werner

2

Nếu bạn hài lòng chỉ cần bắt Control-C, thì đó là một thỏa thuận đã hoàn thành. Nếu bạn thực sự muốn I / O không chặn nhưng bạn không muốn thư viện nguyền rủa, một giải pháp thay thế khác là chuyển khóa, kho và thùng sang thư viện AT&Tsfio . Đó là thư viện đẹp đẽ theo khuôn mẫu trên C stdionhưng linh hoạt hơn, an toàn với luồng và hoạt động tốt hơn. (sfio là viết tắt của I / O an toàn, nhanh chóng.)


1

Không có cách di động nào để làm điều này, nhưng select () có thể là một cách tốt. Xem http://c-faq.com/osdep/readavail.html để biết thêm các giải pháp khả thi.


1
câu trả lời này là không đầy đủ. nếu không có thao tác đầu cuối với tcsetattr () [xem câu trả lời của tôi], bạn sẽ không nhận được gì trên fd 0 cho đến khi bạn nhấn enter.
Alnitak

0

Bạn có thể làm điều đó bằng cách sử dụng select như sau:

  int nfds = 0;
  fd_set readfds;
  FD_ZERO(&readfds);
  FD_SET(0, &readfds); /* set the stdin in the set of file descriptors to be selected */
  while(1)
  {
     /* Do what you want */
     int count = select(nfds, &readfds, NULL, NULL, NULL);
     if (count > 0) {
      if (FD_ISSET(0, &readfds)) {
          /* If a character was pressed then we get it and exit */
          getchar();
          break;
      }
     }
  }

Không quá nhiều việc: D


2
Câu trả lời này là không đầy đủ. Bạn cần đặt thiết bị đầu cuối ở chế độ không chuẩn. Cũng nfdsnên được đặt thành 1.
luser droog

0

Đây là một chức năng để làm điều này cho bạn. Bạn cần termios.hcó hệ thống POSIX.

#include <termios.h>
void stdin_set(int cmd)
{
    struct termios t;
    tcgetattr(1,&t);
    switch (cmd) {
    case 1:
            t.c_lflag &= ~ICANON;
            break;
    default:
            t.c_lflag |= ICANON;
            break;
    }
    tcsetattr(1,0,&t);
}

Chia nhỏ điều này: tcgetattrlấy thông tin đầu cuối hiện tại và lưu trữ nó vào t. Nếu cmdlà 1, cờ đầu vào cục bộ trong tđược đặt thành đầu vào không chặn. Nếu không, nó được đặt lại. Sau đó, tcsetattrthay đổi đầu vào tiêu chuẩn thành t.

Nếu bạn không đặt lại đầu vào chuẩn vào cuối chương trình, bạn sẽ gặp sự cố trong trình bao của mình.


Câu lệnh switch là thiếu một khung :)
étale-cohomology
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.