如何在Linux系统中使用Qt读取设备文件?
嘿,刚好我之前做过Qt和Linux设备文件交互的项目,结合你的游戏手柄需求,给你梳理下Qt里的典型实现方法,再把你提到的基于@rodrigo思路的实现拆解说明下~
Qt与Linux设备文件交互的典型方法
Qt处理Linux设备文件(比如/dev/input/js0这类字符设备),核心是避免阻塞GUI主线程,常用的有两种方案:
方案1:QSocketNotifier + QFile(推荐非阻塞方式)
这种方式利用Linux的文件描述符监听机制,让Qt在设备有数据可读时主动通知我们,完全不会卡UI,非常适合GUI程序:
- 步骤拆解:
- 用
QFile打开目标设备文件,注意要以只读模式打开,并且检查打开是否成功(权限问题很常见)。 - 通过
QFile::handle()获取设备的文件描述符(fd)。 - 创建
QSocketNotifier,监听这个fd的可读事件(QSocketNotifier::Read),并绑定槽函数。 - 当设备有数据时,槽函数会被触发,此时我们再读取数据并处理。
- 用
方案2:QThread + 阻塞读取(适合习惯C风格循环的场景)
如果更熟悉C语言里的阻塞read循环,可以把读取逻辑放到子线程中,避免阻塞主线程的GUI渲染:
- 步骤拆解:
- 自定义一个继承
QThread的类,在run()方法里用C标准库的open()打开设备文件。 - 在
run()里做持续的阻塞read()循环,每次读取到数据后,通过Qt信号把数据发送到主线程。 - 主线程绑定这些信号,更新UI显示。
- 程序退出或需要停止读取时,调用
QThread::requestInterruption()终止循环,并关闭设备文件。
- 自定义一个继承
基于@rodrigo思路的Joystick实现示例
下面是完整的头文件和源文件实现,用的是上面推荐的非阻塞方案:
joystick.h
#ifndef JOYSTICK_H #define JOYSTICK_H #include <QObject> #include <QFile> #include <QSocketNotifier> #include <linux/joystick.h> class Joystick : public QObject { Q_OBJECT public: explicit Joystick(const QString &devicePath = "/dev/input/js0", QObject *parent = nullptr); ~Joystick(); // 判断是否成功连接到手柄设备 bool isConnected() const; signals: // 轴移动信号:axis是轴编号,value是轴的取值(范围-32768到32767) void axisMoved(int axis, qint16 value); // 按键按下信号:button是按键编号 void buttonPressed(int button); // 按键释放信号:button是按键编号 void buttonReleased(int button); private slots: // 读取设备数据的槽函数 void readData(); private: QFile m_deviceFile; QSocketNotifier *m_notifier; bool m_connected; // 标记设备是否连接成功 }; #endif // JOYSTICK_H
joystick.cpp
#include "joystick.h" #include <QDebug> Joystick::Joystick(const QString &devicePath, QObject *parent) : QObject(parent), m_connected(false) { m_deviceFile.setFileName(devicePath); // 以只读方式打开设备文件 if (m_deviceFile.open(QIODevice::ReadOnly)) { int fd = m_deviceFile.handle(); // 创建SocketNotifier监听可读事件 m_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); connect(m_notifier, &QSocketNotifier::activated, this, &Joystick::readData); m_connected = true; qDebug() << "成功连接到手柄设备:" << devicePath; } else { qDebug() << "打开手柄设备失败:" << devicePath << "错误信息:" << m_deviceFile.errorString(); } } Joystick::~Joystick() { // 清理资源 if (m_notifier) { m_notifier->setEnabled(false); delete m_notifier; } if (m_deviceFile.isOpen()) { m_deviceFile.close(); } } bool Joystick::isConnected() const { return m_connected; } void Joystick::readData() { js_event event; // 读取手柄事件结构体 qint64 bytesRead = m_deviceFile.read(reinterpret_cast<char*>(&event), sizeof(event)); if (bytesRead == sizeof(event)) { // 过滤初始化事件(手柄刚连接时会发送一批初始化事件,不需要处理) event.type &= ~JS_EVENT_INIT; switch (event.type) { case JS_EVENT_AXIS: emit axisMoved(event.number, event.value); break; case JS_EVENT_BUTTON: if (event.value == 1) { emit buttonPressed(event.number); } else if (event.value == 0) { emit buttonReleased(event.number); } break; default: break; } } else { qDebug() << "读取手柄事件失败,读取字节数:" << bytesRead; // 出现错误时停止监听,关闭设备 m_notifier->setEnabled(false); m_deviceFile.close(); m_connected = false; } }
一些实用注意事项
- 权限问题:
/dev/input/js0默认需要用户属于input组才能读取,你可以用sudo usermod -aG input 你的用户名给用户添加权限,或者编写udev规则自动设置设备权限。 - 设备重连处理:如果手柄拔掉再插上,你可以定时检查设备文件是否存在,或者用udev监听设备插拔事件,实现自动重连。
- 线程方案补充:如果想用线程方式,
run()方法的核心逻辑大概是这样:
void JoystickThread::run() { int fd = open("/dev/input/js0", O_RDONLY); if (fd == -1) { emit errorOccurred("打开手柄设备失败"); return; } js_event event; while (!isInterruptionRequested()) { ssize_t bytes = read(fd, &event, sizeof(event)); if (bytes == sizeof(event)) { event.type &= ~JS_EVENT_INIT; switch (event.type) { case JS_EVENT_AXIS: emit axisMoved(event.number, event.value); break; case JS_EVENT_BUTTON: if (event.value == 1) { emit buttonPressed(event.number); } else if (event.value == 0) { emit buttonReleased(event.number); } break; } } else { emit errorOccurred("读取手柄数据失败"); break; } } close(fd); }
内容的提问来源于stack exchange,提问作者Alex44
相关产品推荐
相关产品推荐

