Ncurses and Qt Interoperability

Ncurses and Qt Interoperability

  1. Use QSocketNotifier to be notified of things being available on stdin.

  2. Call nonblocking getch() in a loop until no more input is available. This is vitally important: the notifier will notify only when new data is available, but this doesn't mean that it notifies on every character! If you receive multiple characters at a time, you will usually get just one notification - thus you must keep issuing non-blocking getch() until it returns ERR meaning that no more data is available at the moment.

  3. You should also read all of the data that became available in the time before the socket notifier became attached.

The code below echoes the input as it receives it, and additionally outputs a * every second. This works on Linux and OS X, and is not portable to Windows. To quit, press Q.

Using ncurses for a legacy text-mode user interface, where desired, while leveraging Qt for everything else (timing, networking, data models with text-based views, XML, QObjects, etc.) is a perfectly valid approach.

// https://github.com/KubaO/stackoverflown/tree/master/questions/ncurses-20606318
#include <QtCore>
#include <ncurses.h>

class Worker : public QObject
{
Q_OBJECT
QSocketNotifier m_notifier{0, QSocketNotifier::Read, this};
QBasicTimer m_timer;
Q_SLOT void readyRead() {
// It's OK to call this with no data available to be read.
int c;
while ((c = getch()) != ERR) {
printw("%c", (char)(c <= 255 ? c : '?'));
if (c == 'q' || c == 'Q') qApp->quit();
}
}
void timerEvent(QTimerEvent * ev) {
if (ev->timerId() != m_timer.timerId()) return;
printw("*");
refresh();
}
public:
Worker(QObject * parent = 0) : QObject(parent) {
connect(&m_notifier, SIGNAL(activated(int)), SLOT(readyRead()));
readyRead(); // data might be already available without notification
m_timer.start(1000, this);
}
};

int main(int argc, char *argv[])
{
QCoreApplication a{argc, argv};
Worker w;
auto win = initscr();
clear();
cbreak(); // all input is available immediately
noecho(); // no echo
printw("Press <q> to quit\n");
keypad(win, true); // special keys are interpreted and returned as single int from getch()
nodelay(win, true); // getch() is a non-blocking call
auto rc = a.exec();
endwin();
return rc;
}

#include "main.moc"

Using QML for creating curses applications

Qt can be (Ncurses and Qt Interoperability) used to handle the async I/O details of an ncurses app even without QML nor any other high-level frameworks.

You could also expose some C++ class framework for text interfaces via QML. For example, as was done in the presentation, you could expose CDK - the Curses Development Kit to QML.

I couldn't find the code from the presentation, but re-implementing it shouldn't be too hard. All you do is wrap CDK objects in QObjects, properly exposing the properties as Qt properties.

ncurses, print and contemporary acquire strings

ncurses provides functions for reading a single character with a timeout (or even no delay at all). Those are not useful with scanw (which always blocks, waiting for several characters):

  • when using a timeout, getch returns ERR rather than a character when the timeout expires.
  • scanw may use getch internally, but has no way to continue when getch returns ERR.

Rather than block, you can poll for input. If no character is available within a given time interval, your program gives up (for the moment), and does something more useful than waiting for characters.
Applications that poll for input can be written to accept characters one at a time, adding them to a buffer until the user presses Enter, and then run sscanf on the completed text.

The regular C sscanf is used (which reads from a string) rather than reading directly from the screen with scanw.

Qt : which class should connect() signals and slots - inside or outside the view class?

One of the purposes of object-oriented design is encapsulation. To the user of the view class it is irrelevant how you implemented the class. The design should focus on a convenient interface first. You can then have one implementation that uses widget controls, another can be ncurses-based, yet another can use QML, etc. This means that ideally nothing about the class's insides should be visible at the level of the interface.

The whole point of the signal-slot mechanism was, in fact, decoupling the classes involved in the connections: the classes being connected need to know nothing about each other. It is thus a sensible starting point, and indeed usually correct, to set up the connections from outside of the classes.

To help you in this task, you can leverage the signal-signal connections. In Qt, a signal is just a method whose implementation is machine-generated. It has a different designation in the metadata, but it is an invokable method just as a slot is. So, when considered as a target of a connection, the signals and slots are equivalent. You can connect signals to signals. A signal-signal connection simply invokes the target signal method when the source signal method is invoked.

How to get QIODevice-like signals for console input (stdin)?

For Unix (Linux, OS X), this answer has a solution.

For Windows, you cannot use QSocketNotifier: instead, use a QWinEventNotifier on the console handle you obtain from GetStdHandle (see this answer). The code will be almost identical to the Unix variant from the answer above.

How to subclass QIODevice and read /dev/input/eventX

Use QSocketNotifier on the open file handle. You can read from the device using QFile,or abuse QSerialPort, i.e. QSerialPort m_port{"input/eventX"}. See this answer for an example of using QSocketNotifier with stdin; /dev/input/eventX requires a similar approach.

Here's an example that works on /dev/stdio, but would work identically on /dev/input/eventX.

// https://github.com/KubaO/stackoverflown/tree/master/questions/dev-notifier-49402735
#include <QtCore>
#include <fcntl.h>
#include <boost/optional.hpp>

class DeviceFile : public QFile {
Q_OBJECT
boost::optional<QSocketNotifier> m_notifier;
public:
DeviceFile() {}
DeviceFile(const QString &name) : QFile(name) {}
DeviceFile(QObject * parent = {}) : QFile(parent) {}
DeviceFile(const QString &name, QObject *parent) : QFile(name, parent) {}
bool open(OpenMode flags) override {
return
QFile::isOpen()
|| QFile::open(flags)
&& fcntl(handle(), F_SETFL, O_NONBLOCK) != -1
&& (m_notifier.emplace(this->handle(), QSocketNotifier::Read, this), true)
&& m_notifier->isEnabled()
&& connect(&*m_notifier, &QSocketNotifier::activated, this, &QIODevice::readyRead)
|| (close(), false);
}
void close() override {
m_notifier.reset();
QFile::close();
}
};

int main(int argc, char **argv) {
QCoreApplication app{argc, argv};
DeviceFile dev("/dev/stdin");
QObject::connect(&dev, &QIODevice::readyRead, [&]{
qDebug() << "*";
if (dev.readAll().contains('q'))
app.quit();
});
if (dev.open(QIODevice::ReadOnly))
return app.exec();
}

#include "main.moc"


Related Topics



Leave a reply



Submit