在 Linux 操作系统上,有多种输入系统可以与 Qt 一起使用。
Qt 内置支持以下类型的触摸屏界面:
evdev:事件设备接口
libinput:处理输入设备的库
tslib:打字稿运行时库
我们将从学习Linux evdev系统开始,直接读取设备文件。
evdev
Qt 内置支持 Linux 和嵌入式 Linux 的 evdev 标准事件处理系统。 如果没有配置或检测到其他系统,这就是默认情况下您将获得的结果。 它处理键盘、鼠标和触摸。 然后您可以像往常一样使用 Qt 处理键盘、触摸和鼠标事件。
您可以分配启动参数,例如设备文件路径和屏幕默认旋转,如下所示:
QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/input2:rotate=90
其他可用的参数是 invertx 和 inverty。 当然,这些输入事件不需要在Qt上回复,直接在Qt下面的栈中访问即可。 我称它们为原始事件,但它们实际上只是读取特殊的 Linux 内核设备文件。
让我们来看看在使用 Qt 时自己处理这些 evdev 输入事件。 这是低级系统文件访问,因此您可能需要 root 或管理员权限才能运行以这种方式使用它的应用程序。
Linux 上的输入事件通过内核的 dev 节点访问,通常位于 /dev/input 中,但它们可以位于 /dev 目录树下的任何位置,具体取决于驱动程序。 QFile 不应用于实际读取这些特殊设备节点文件。
读取输入节点的主要包含文件如下:
#include <linux/input.h>
您需要扫描设备文件以检测触摸屏生成的文件。 在 Linux 中,这些设备节点是动态命名的,因此您需要使用其他方法来识别正确的文件,而不仅仅是文件名。 因此,您必须打开该文件并要求它告诉您它的名称。
我们可以使用 QDir 及其过滤器至少过滤掉一些我们知道不是我们正在寻找的文件:
QDir inputDir = QDir("/dev/input");
QStringList filters;
filters << "event*";
QStringList eventFiles = inputDir.entryList(filters,
QDir::System);
int fd = -1;
char name[256];
for (QString file : eventFiles) {
file.prepend(inputDir.absolutePath());
fd = ::open(file.toLocal8Bit().constData(), O_RDONLY|O_NONBLOCK);
if (fd >= 0) {
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
::close(fd);
}
}
确保包含 O_NONBLOCK 参数来打开。
此时,我们有一个不同输入设备的名称列表。 您可能只需要猜测要使用的名称,然后进行字符串比较即可找到正确的设备。 有时,驱动程序会有正确的 id 信息,可以像这样使用 EVIOCGID 获取:
unsigned short id[4];
ioctl(fd, EVIOCGID, &id);
有时,您可以使用 EVIOCGBIT 检测某些功能。 这将告诉我们硬件驱动程序支持哪些按钮或键。 当您触摸它时,触摸屏驱动程序会输出 0x14a (BTN_TOUCH) 的键码,因此我们可以使用它来检测哪个输入事件将成为我们的触摸屏:
bool MainWindow::isTouchDevice(int fd)
{
unsigned short id[4];
long bitsKey[LONG_FIELD_SIZE(KEY_CNT)];
memset(bitsKey, 0, sizeof(bitsKey));
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(bitsKey)), bitsKey);
if (testBit(BTN_TOUCH, bitsKey)) {
return true;
}
return false;
}
我们现在可以相当确定我们有正确的设备文件。 现在,我们可以设置一个 QSocketNotifier 对象来在该文件被激活时通知我们,然后我们可以读取它以获取触摸的 X 和 Y 值。 我们使用 QSocketNotifier 类是因为我们不能使用 QFile,因为它没有任何信号来告诉我们 Linux 设备文件何时更改,因此这使它变得更容易:
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read,
this);
auto c = connect(notifier, &QSocketNotifier::activated,
[=]( int /*socket*/ ) {
struct input_event ev;
unsigned int size;
size = read(fd, &ev, sizeof(struct input_event));
if (size < sizeof(struct input_event)) {
qWarning("expected %u bytes, got %u\n", sizeof(struct
input_event), size);
perror("\nerror reading");
return EXIT_FAILURE;
}
if (ev.type == EV_KEY && ev.code == BTN_TOUCH)
qWarning("Touchscreen value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_X)
qWarning("X value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_Y)
qWarning("Y value: %i\n", ev.value);
return 0;
});
return true;
}
我们还使用标准的 read() 函数而不是 QFile 来读取它。
BTN_TOUCH 事件值告诉我们何时按下或释放触摸屏。
ABS_MT_POSITION_X 值将是触摸屏的 X 位置,而 ABS_MT_POSITION_Y 值将是 Y 位置。
有一个库可以用来做同样的事情,这可能会更容易一些。
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
bool isTouchDevice(int fd);
void scanInputDevices();
int doScan(int fd);
int fd;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDir>
#include <QFile>
#include <QSocketNotifier>
#include <QTextStream>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <linux/input.h>
#include <QDebug>
#define BITSLONG (sizeof(long) * 8)
#define LONG_FIELD_SIZE(bits) ((bits / BITSLONG) + 1)
#define BTN_TOUCH 0x14a
#define ABS_MT_POSITION_X 0x35
#define ABS_MT_POSITION_Y 0x36
static inline bool testBit(long bit, const long *array)
{
return (array[bit / BITSLONG] >> bit % BITSLONG) & 1;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
fd(-1)
{
ui->setupUi(this);
scanInputDevices();
}
MainWindow::~MainWindow()
{
::close(fd);
delete ui;
}
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read, this);
auto c = connect(notifier, &QSocketNotifier::activated,
[=]( int /*socket*/ ) {
struct input_event ev;
unsigned int size;
size = read(fd, &ev, sizeof(struct input_event));
if (size < sizeof(struct input_event)) {
qWarning("expected %u bytes, got %u\n", sizeof(struct input_event), size);
perror("\nerror reading");
return EXIT_FAILURE;
}
if (ev.type == EV_KEY && ev.code == BTN_TOUCH)
qWarning("Touchscreen value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_X)
qWarning("X value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_Y)
qWarning("Y value: %i\n", ev.value);
return 0;
});
return true;
}
void MainWindow::scanInputDevices()
{
QDir inputDir = QDir("/dev/input");
QStringList filters;
filters << "event*";
QStringList eventFiles = inputDir.entryList(filters, QDir::System);
char name[256];
for (QString file : eventFiles) {
file.prepend("/dev/input/");
fd = ::open(file.toLocal8Bit().constData(), O_RDONLY);
if (fd >= 0) {
if (isTouchDevice(fd)) {
doScan(fd);
break;
}
}
}
}
bool MainWindow::isTouchDevice(int fd)
{
unsigned short id[4];
long bitsKey[LONG_FIELD_SIZE(KEY_CNT)];
memset(bitsKey, 0, sizeof(bitsKey));
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(bitsKey)), bitsKey);
if (testBit(BTN_TOUCH, bitsKey)) {
return true;
}
return false;
}
libevdev
当您使用库 libevdev 时,您将不必访问诸如 QSocketNotifier 之类的低级文件系统函数并自己读取文件。
要使用 libevdev,我们首先在项目 .pro 文件中添加 LIBS 条目。
LIBS += -levdev
这允许 qmake 设置正确的链接器参数。 包含标头如下
#include <libevdev-1.0/libevdev/libevdev.h>
我们可以借用前面代码中的初始代码来扫描设备文件的目录,但是 isTouchDevice 函数得到了更清晰的代码:
bool MainWindow::isTouchDevice(int fd)
{
int rc = 1;
rc = libevdev_new_from_fd(fd, &dev);
if (rc < 0) {
qWarning("Failed to init libevdev (%s)\n", strerror(-rc));
return false;
}
if (libevdev_has_event_code(dev, EV_KEY, BTN_TOUCH)) {
qWarning("Device: %s\n", libevdev_get_name(dev));
return true;
}
libevdev_free(dev);
return false;
}
libevdev 有很好的 libevdev_has_event_code 函数,可以用来轻松检测设备是否有特定的事件代码。 这正是我们识别触摸屏所需要的! 注意 libevdev_free 函数,它将释放我们不需要的正在使用的内存。
doScan 函数失去对 read 的调用,但代之以对 libevdev_next_event 的调用。 它还可以通过调用 libevdev_event_code_get_name 输出带有事件代码实际名称的好消息:
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read,
this);
auto c = connect(notifier, &QSocketNotifier::activated,
[=]( int /*socket*/ ) {
int rc = -1;
do { struct input_event ev;
rc = libevdev_next_event(dev,
LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == LIBEVDEV_READ_STATUS_SYNC) {
while (rc == LIBEVDEV_READ_STATUS_SYNC) {
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_SYNC, &ev);
}
} else if (rc == LIBEVDEV_READ_STATUS_SUCCESS) {
if ((ev.type == EV_KEY && ev.code == BTN_TOUCH) ||
(ev.type == EV_ABS && ev.code ==
ABS_MT_POSITION_X) ||
(ev.type == EV_ABS && ev.code ==
ABS_MT_POSITION_Y)) {
qWarning("%s value: %i\n",
libevdev_event_code_get_name(ev.type, ev.code), ev.value);
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
return 0;
});
return 0;
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <libevdev-1.0/libevdev/libevdev.h>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
void scanInputDevices();
bool isTouchDevice(int fd);
int doScan(int fd);
int fd;
struct libevdev *dev = NULL;
//private slots:
// void activated(int);
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDir>
#include <QFile>
#include <QSocketNotifier>
#include <fcntl.h>
#include <unistd.h>
#include <QDebug>
//#define BTN_TOUCH 0x14a
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
fd(-1)
{
ui->setupUi(this);
scanInputDevices();
}
MainWindow::~MainWindow()
{
libevdev_free(dev);
delete ui;
}
void MainWindow::scanInputDevices()
{
QDir inputDir = QDir("/dev/input");
QStringList filters;
filters << "event*";
QStringList eventFiles = inputDir.entryList(filters, QDir::System);
char name[256];
for (QString file : eventFiles) {
file.prepend("/dev/input/");
fd = ::open(file.toLocal8Bit().constData(), O_RDONLY|O_NONBLOCK);
if (fd >= 0) {
if (isTouchDevice(fd)) {
doScan(fd);
break;
} else {
::close(fd);
}
}
}
}
bool MainWindow::isTouchDevice(int fd)
{
int rc = 1;
rc = libevdev_new_from_fd(fd, &dev);
if (rc < 0) {
qWarning("Failed to init libevdev (%s)\n", strerror(-rc));
return false;
}
if (libevdev_has_event_code(dev, EV_KEY, BTN_TOUCH)) {
qWarning("Device: %s\n", libevdev_get_name(dev));
return true;
}
libevdev_free(dev);
return false;
}
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read, this);
auto c = connect(notifier, &QSocketNotifier::activated,
[=]( int /*socket*/ ) {
int rc = -1;
do {
struct input_event ev;
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == LIBEVDEV_READ_STATUS_SYNC) {
while (rc == LIBEVDEV_READ_STATUS_SYNC) {
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_SYNC, &ev);
}
} else if (rc == LIBEVDEV_READ_STATUS_SUCCESS) {
if ((ev.type == EV_KEY && ev.code == BTN_TOUCH) ||
(ev.type == EV_ABS && ev.code == ABS_MT_POSITION_X) ||
(ev.type == EV_ABS && ev.code == ABS_MT_POSITION_Y)) {
qWarning("%s value: %i\n", libevdev_event_code_get_name(ev.type, ev.code), ev.value);
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
return 0;
});
return 0;
}