等待终端输入时如何处理窗口事件?

3

我有一个跨平台的应用程序(Windows和Unix + xcb),它包含终端和图形窗口,并且大多数情况下都正常工作,但如果您在输入提示符处等待时间过长,在重负载下图像可能会消失。 :(

我为解释器(Postscript解释器)编写了一个主循环(REPL),每次在其循环中调用事件处理程序函数。该事件处理程序执行通常是窗口消息/事件循环的迭代之一。但是输入使用普通的C I/O处理,因此当在fgetc()阻塞时,事件处理程序永远不会被调用。

图形窗口仅输出内容。它没有按钮,并且只需要响应Raise、Map、Expose等事件。

如何安排在调用堆栈中更深的输入读取循环期间调用事件处理程序?这需要能够使用POSIX和win32 API实现。

选项似乎是

  • 非阻塞I/O
    在Unix中相对简单,看起来在Windows上很麻烦
  • 轮询
  • 输入线程
    pthreads?
  • 窗口线程
    pthreads?

这些选项中是否有任何一个比其他选项更容易实现?

如果我可以一直使用Unix,那么这似乎就能解决所有问题:

#include <errno.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

void idleproc () {  /* simulates calling the event handler 
                        (ie. one slice of the window loop) */
    //printf("idle\n");
    putchar('.');
}

int idlefgetc (FILE *stream) {
    int ret;

    do {
        ret = fgetc(stream);
        idleproc();
    } while(ret == EOF && 
            (errno == EAGAIN || errno == EINTR));

    return ret;
}

int setraw (FILE *stream) {
    struct termios tbuf;
    if (tcgetattr(fileno(stream), &tbuf) == -1)
        return -1;
    tbuf.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
            | INLCR | IGNCR | ICRNL | IXON);
    tbuf.c_oflag &= ~OPOST;
    tbuf.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tbuf.c_cflag &= ~(CSIZE | PARENB);
    tbuf.c_cflag |= CS8;
    if (tcsetattr(fileno(stream), TCSANOW, &tbuf) == -1)
        return -1;
    return 0;
}

int setnonblocking (FILE *stream) {
    int flags;

    if (setraw(stream) != 0)
        return -1;
    if (!((flags = fcntl(fileno(stream), F_GETFL)) & O_NONBLOCK)) {
        flags |= O_NONBLOCK;
        fcntl(fileno(stream), F_SETFL, flags);
    }
    return 0;
}

int main (int argc, char **argv) {
    if (setnonblocking(stdin)) {
        perror(argv[0]);
        return 0;
    }
    printf("'%d'\n", idlefgetc(stdin));
    system("stty sane");
    return 0;
}

1
GUI应用程序通常具有事件循环。如果您还在终端上拥有REPL,则两个循环应该在不同的线程中运行,具体取决于它们如何交互。为了保持用户界面的响应性,GUI通常不应在与重型计算相同的线程中运行。 - amon
该程序调用绘制GUI窗口,但窗口本身应该自行处理。 - luser droog
这段代码基于在Linux上实现getch的技术,该技术在我的答案中有描述。 - luser droog
2个回答

5
在Windows下,您需要使用控制台API。您可以使用ReadFileEx异步、非阻塞地读取字符。另一种可能性是ReadConsoleInput,并持续轮询输入,而不会阻塞。使用SetConsole,您可以决定捕获哪些事件。
有关在Windows下进行异步I/O的更多详细信息,请参见此处

谢谢!这看起来就是我要找的东西。我在搜索时找到的所有内容都集中在从套接字读取方面。 - luser droog

2
另一种解决方案似乎是可能的,因为毕竟这是一个编程语言解释器。应该能够重新实现使用fgetc的代码以代替使用ps原语:-file-readint bool。这个postscript运算符本身使用stdio调用,但可以通过在执行堆栈上推送并返回来由其他文件读取函数以续传方式调用。这将自然地交错更多对事件处理程序的调用,因为它更频繁地返回到主循环。
我可能还需要使用非阻塞读取。但如果只在一个地方调用它,那么管理起来会更容易。续传具有将较大的函数分解成单独的阶段的优点,并已成功地用于通过覆盖基类中的方法来实现窗口设备(基类实现为postscript字典)。但这对我来说仍然相当新颖,所以目前还不是我的首选“万能胶带”。 :)
下班后我会重新制作这个原型以说明这种方法。
编辑:花了几天时间。但这里是新想法。对于窗口,它需要以不同的方式进行非阻塞调用,但调用可以隔离到这一个地方。而且通过续传,文件读取函数不需要访问(或了解)事件处理程序,因此封装更好。
这个程序的行为与问题中的程序相同,它重复打印直到按下键盘,然后打印键盘按键的ascii代码。 模拟在等待按键时重复调用事件处理程序。我必须mock-up一些解释器内容:对象类型、一些堆栈和一个eval()函数。因此,这也更好地说明了REPL。
#include <errno.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

int set_raw_term (FILE *stream) {
    struct termios tbuf;
    if (tcgetattr(fileno(stream), &tbuf) == -1) 
        return -1; 
    tbuf.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
            | INLCR | IGNCR | ICRNL | IXON);
    tbuf.c_oflag &= ~OPOST;
    tbuf.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tbuf.c_cflag &= ~(CSIZE | PARENB);
    tbuf.c_cflag |= CS8;
    if (tcsetattr(fileno(stream), TCSANOW, &tbuf) == -1) 
        return -1; 
    return 0;
}

int set_nonblocking (FILE *stream) {
    int flags;

    if (set_raw_term(stream) != 0)
        return -1; 
    if (!((flags = fcntl(fileno(stream), F_GETFL)) & O_NONBLOCK)) {
        flags |= O_NONBLOCK;
        fcntl(fileno(stream), F_SETFL, flags);
    }   
    return 0;
}

int event_handler() {
    putchar('.');
}

enum { null, integer, file, operator };
typedef union {
    short tag;
    struct {
        short tag;
        int val;
    } int_;
    struct {
        short tag;
        FILE *f; 
    } file_;
    struct {
        short tag;
        int (*fp)();
    } oper_;
} object; /* object union allows multiple types on the stacks */

object os[100];   /* operand stack */
object *tos = os; /* top of operand stack */
object es[100];   /* execution stack */
object *tes = es; /* top of execution stack */

int eval () {
    if (tes == es) /* execution stack is empty */
        return -1; /* return "finished" */

    event_handler();

    switch(tes[-1].tag) { /* type of object on top of execution stack */
        case integer:
        case file:
            *tos++ = *--tes;     /* push file or integer to operand stack */
            break;

        case operator:
            (--tes)->oper_.fp(); /* call operator function */
            break;
    }
    return 0; /* return "not finished" */
}

int file_read_byte () {
    int ret;
    object arg;

    arg = *--tos; /* pop argument from operand stack */
    ret = fgetc(arg.file_.f);
    if (ret == EOF && (errno == EAGAIN || errno == EINTR)) { /* if no data */
        *tos++ = arg;   /* restore argument to operand stack */
        *tes++ = (object){ .oper_.tag = operator, .oper_.fp = file_read_byte }; /* push continuation to execution stack */
        return 0;
    } else {
        *tos++ = (object){ .int_.tag = integer, .int_.val = ret }; /* push result to operand stack */
        return 0;
    }
}


int main(int argc, char **argv) {
    int ret;

    if (set_nonblocking(stdin) != 0) {
        perror(argv[0]);
        return 0;
    }

    //printf("'%d'\n", file_read_byte(stdin));
    *tos++ = (object){ .file_.tag = file, .file_.f = stdin }; /* push file argument to operand stack */
    *tes++ = (object){ .oper_.tag = operator, .oper_.fp = file_read_byte }; /* push operator object to execution stack */

    ret = 0;
    while (ret == 0) { /* call eval until execution is "finished" */
        ret = eval();
    }
    printf("'%d'\n", (--tos)->int_.val); /* pop returned value */

    system("stty sane");
    return 0;
}

非常感谢您分享您的想法! - jpmuc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接