希望能够实现自动测试,自动测试录制操作,录制完成后即可启动自动测试,当然可以定义更多的何时启动测试、测试结束之后重启等操作,目标平台是linux arm Qt4.8触屏设备,暂不考虑按键操作。
如何去实现呢?
- 事件录制
首先需要将鼠标事件都记录下来,如何记录呢?继承自 QApplication 实现 MyApplication ,重写它的 bool notify(QObject *obj, QEvent *event) 函数,通过这个函数,我们能在事件派发到控件之前,拿到发送出去的事件,这时候将其事件类型、事件位置记录下来即可,需要注意的是,当在某个控件鼠标按下的时候,实际上是先产生一个 mouseMove 事件移动到这个控件,然后再 mousePress 的,代码如下:
#include "MyApplication.h"
MyApplication::MyApplication(int & argc, char ** argv): QApplication(argc, argv)
{}
bool MyApplication::notify(QObject * obj, QEvent * event)
{
if(event->type() == QMouseEvent::MouseButtonPress || event->type() == QMouseEvent::MouseButtonRelease ||
event->type() == QEvent::MouseMove){
if(isTestRecording && obj->objectName() != "btnTestRecord" && !obj->objectName().isEmpty()){
QMouseEvent * ev = (QMouseEvent *)event;
LOGOUT<<obj<<event->type()<<ev->pos()<<ev->globalPos();
emit TestRecordOperate(event->type(),ev->pos(),ev->globalPos());//事件发送出去,记录下来
}
}
return QApplication::notify(obj,event);
}
- 回放
如何将录制下来的事件,重新回放呢?
想法一:最理想的方式是,我记录事件和位置时,只记录事件的类型和事件的全局位置,然后发送给Qt程序,由Qt程序自己定位会发送到哪个控件,会触发什么消息。那如何发送呢?在这个 linux arm 设备上,使用 qtopia4 启动程序,使用 tslib 处理事件,事件从 /dev/input/event0 中来的,未找到直接往程序发送原始事件的方法,那试着向 /dev/input/event0 写入内容试试?使用类似如下代码写入:
void AutoTestManager::SendMouseEvent(bool press)
{
LOGOUT;
event_.type = EV_KEY;
event_.value = press;
event_.code = BTN_LEFT;
LOGOUT;
WriteEvent();
LOGOUT;
SendSyn();
LOGOUT;
}
void AutoTestManager::SendMoveEvent(int x, int y)
{
LOGOUT;
event_.type = EV_REL;/*EV_REL*/
event_.value = x;
event_.code = REL_X;
LOGOUT;
WriteEvent();
LOGOUT;
event_.type = EV_REL;/*EV_ABS*/
event_.value = y;
event_.code = REL_Y;
LOGOUT;
WriteEvent();
LOGOUT;
SendSyn();
LOGOUT;
}
void AutoTestManager::SendSyn()
{
LOGOUT;
event_.type = EV_SYN;
event_.value = 0;
event_.code = SYN_REPORT;
LOGOUT;
WriteEvent();
LOGOUT;
}
void AutoTestManager::WriteEvent()
{
gettimeofday(&event_.time, 0);
LOGOUT;
char buf[100] = {0};
LOGOUT;
//sprintf(buf,"%02X%02X %02X%02X %02X%02X %02X%02X %02X%02X %02X%02X %02X%02X %02X%02X",((char*)(&event_))[0],((char*)(&event_))[1],((char*)(&event_))[2],
// ((char*)(&event_))[3],((char*)(&event_))[4],((char*)(&event_))[5],((char*)(&event_))[6],((char*)(&event_))[7],((char*)(&event_))[8],((char*)(&event_))[9],
// ((char*)(&event_))[10],((char*)(&event_))[11],((char*)(&event_))[12],((char*)(&event_))[13],((char*)(&event_))[14],((char*)(&event_))[15]);
LOGOUT;
char bbbb[16];
bbbb[0] = ((char*)(&event_))[1] ;
bbbb[1] = ((char*)(&event_))[0] ;
bbbb[2] = ((char*)(&event_))[3] ;
bbbb[3] = ((char*)(&event_))[2] ;
bbbb[4] = ((char*)(&event_))[5] ;
bbbb[5] = ((char*)(&event_))[4] ;
bbbb[6] = ((char*)(&event_))[7] ;
bbbb[7] = ((char*)(&event_))[6] ;
bbbb[8] = ((char*)(&event_))[9] ;
bbbb[9] = ((char*)(&event_))[8] ;
bbbb[10] = ((char*)(&event_))[11];
bbbb[11] = ((char*)(&event_))[10];
bbbb[12] = ((char*)(&event_))[13];
bbbb[13] = ((char*)(&event_))[12];
bbbb[14] = ((char*)(&event_))[15];
bbbb[15] = ((char*)(&event_))[14];
sprintf(buf,"%02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x",bbbb[0],bbbb[1],bbbb[2],
bbbb[3],bbbb[4],bbbb[5],bbbb[6],bbbb[7],bbbb[8],bbbb[9],
bbbb[10],bbbb[11],bbbb[12],bbbb[13],bbbb[14],bbbb[15]);
LOGOUT<<write(fdTouch_, bbbb, sizeof(event_))<<buf;
LOGOUT;
char aaaa[64] = {0};
LOGOUT<<read(fdTouch_,aaaa,64);
LOGOUT;
}
但是很遗憾,这个方法写入没有保错,返回值都正常,但是Qt程序毫无反应,使用 hexdump /dev/input/event0 也毫无反应,猜测可能是驱动虚拟的文件,与实际文件不一样吧。。。反正是没走通的.
想法二:既然没有办法直接往程序加入全局坐标的事件,那就只能在程序中实现,自己查找全局坐标下的控件,然后再往控件相对坐标事件。如何查找全局坐标下的控件呢?有两个 API 可以参考:
1、QWidget *QWidget::childAt(const QPoint &p):
2、QWidget *QApplication::widgetAt(int x, int y):
事实上 Qt 在定位事件的时候用的也是 widgetAt 的,当然要复杂的多…,childAt 也试了,有些问题…,所以最终用 widgetAt 实现,
大概实现代码:
.h
#ifndef AUTOTESTMANAGER_H
#define AUTOTESTMANAGER_H
#include <QObject>
#include <QFile>
#include <QEvent>
#include <QTimer>
#include "Public/PublicStructs.h"
const static int POSSTOP = -1; //表示停止
struct TestStruct
{
TestStruct(int cmd,int x,int y,int time) {
this->cmd = cmd;
this->x = x;
this->y = y;
this->time = time;
}
bool IsValid(){
if(cmd == QEvent::MouseButtonPress || cmd == QEvent::MouseButtonRelease || cmd == QEvent::MouseMove){
return /*x >= 0 && x <= 1080 && y >= 0 && y <= 1920 &&*/ time > 0;
}
return cmd == QEvent::None && time > 0;
}
QString Print(){
return QString(" cmd:%1 x:%2 y:%3 time:%4").arg(cmd).arg(x).arg(y).arg(time);
}
QString ToString(){
return QString("%1 %2 %3 %4\n").arg(cmd).arg(x).arg(y).arg(time);
}
int cmd;
int x;
int y;
int time; //msec
};
class MainWindow;
class AutoTestManager : public QObject
{
Q_OBJECT
public:
explicit AutoTestManager(QObject *parent = 0);
~AutoTestManager();
private slots:
void OnTestRecordOperate(int type,QPoint pos,QPoint gPos);
void OnTestRecord(bool finish,bool start,bool reboot);
void OnTestStart(bool start = true);
void TestTimeOut();
private:
int GetTimeSplit();
bool InitData();
void SaveData();
bool CheckValid();
void Print();
private:
std::vector<TestStruct> vec;
QDateTime time_;
int curPos_;
MainWindow * parent_;
QWidget * widget_;
};
#endif // AUTOTESTMANAGER_H
.cpp
#include "AutoTestManager.h"
#include <QMouseEvent>
#include <QApplication>
#include "Main/MainWindow.h"
AutoTestManager::AutoTestManager(QObject *parent) : QObject(parent)
{
LOGOUT<<parent<<this->parent();
widget_ = NULL;
curPos_ = POSSTOP;
parent_ = (MainWindow *)parent;
time_ = QDateTime::currentDateTime();
InitData();
//Print();
if(vec.size() > 2 && vec[vec.size() - 1].x == 1){ //start on the program begin.
SIGSHOT(5000,OnTestStart);
}
}
AutoTestManager::~AutoTestManager()
{
}
void AutoTestManager::OnTestRecordOperate(int type, QPoint pos, QPoint gPos)
{
if(vec.size() > 0){
vec[vec.size() - 1].time = GetTimeSplit();
}
if(type == QEvent::MouseButtonPress && vec.size() > 0){ //移除press之前的move消息
int time = 1;
while (vec.size() > 0){
if(vec[vec.size() - 1].cmd == QEvent::MouseMove){
time += vec[vec.size() - 1].time;
vec.erase(vec.begin() + vec.size() - 1); //erase the last move.
LOGOUT<<"record erased."<<vec.size()<<time;
}else{
break;
}
}
vec.push_back(TestStruct(QEvent::MouseMove,gPos.x(),gPos.y(),10));
vec.push_back(TestStruct(QEvent::MouseMove,pos.x(),pos.y(),time));
}
vec.push_back(TestStruct(type,pos.x(),pos.y(),1));
LOGOUT<<vec[vec.size() - 1].Print()<<vec.size();
}
void AutoTestManager::OnTestRecord(bool finish, bool start, bool reboot)
{
curPos_ = POSSTOP;
widget_ = NULL;
if(finish){ //录制结束
vec.push_back(TestStruct(QEvent::None,start,reboot,10));
Print();
SaveData();
}else{ //开始录制
vec.clear();
time_ = QDateTime::currentDateTime();
vec.push_back(TestStruct(QEvent::None,0,0,GetTimeSplit()));
}
LOGOUT<<vec[vec.size() - 1].Print();
}
void AutoTestManager::OnTestStart(bool start)
{
LOGOUT<<start;
if(start){
if(!GlobalData::IsTestRecording() && curPos_ == POSSTOP && vec.size() > 1){
curPos_ = 0;
widget_ = NULL;
SIGSHOT(1000,TestTimeOut);
}
}else{
curPos_ = POSSTOP;
}
}
void AutoTestManager::TestTimeOut()
{
if(curPos_ > POSSTOP && curPos_ < vec.size()){
TestStruct * test = &vec[curPos_];
LOGOUT<<curPos_<<test->Print();
if(curPos_ > 0 && (test->cmd == QEvent::MouseButtonPress || test->cmd == QEvent::MouseButtonRelease ||
test->cmd == QEvent::MouseMove)){
if(!widget_ && test->cmd == QEvent::MouseMove){ //find
widget_ = QApplication::widgetAt(test->x,test->y);
LOGOUT<<"widgetAt:"<<curPos_<<test->Print()<<widget_;
}else if(widget_ && !widget_->objectName().isEmpty()){
QMouseEvent ev((QEvent::Type)test->cmd,QPoint(test->x,test->y),Qt::LeftButton,Qt::LeftButton,Qt::NoModifier);
LOGOUT<<widget_<<curPos_<<test->Print()<<QApplication::sendEvent(widget_,&ev);
}
if(!widget_ || (widget_ && widget_->objectName().isEmpty())){
LOGOUT<<"ERROR: widget is NULL or object name is empty."<<widget_<<curPos_<<test->Print();
parent_->OnTestSendEvent((QEvent::Type)test->cmd,test->x,test->y);
}
widget_ = test->cmd == QEvent::MouseButtonRelease ? NULL : widget_;
}else if(curPos_ == vec.size() - 1){
widget_ = NULL;
if(test->y == 1){
VecsClient::RemoteClient()->SetReboot(true);
LOGOUT<<"reboot.";
return;
}
}
++curPos_;
if(curPos_ < vec.size()){
SIGSHOT(test->time,TestTimeOut);
}
}
}
int AutoTestManager::GetTimeSplit()
{
QDateTime time = QDateTime::currentDateTime();
int ret = time_ >= time ? 1 : time.toMSecsSinceEpoch() - time_.toMSecsSinceEpoch();
time_ = time;
return ret;
}
bool AutoTestManager::InitData()
{
vec.clear();
QFile file("/mnt/opt/Qtopia4.8.6/bin/testRecord.dat");
if(file.open(QIODevice::ReadWrite)){
QStringList list = QString(file.readAll()).split('\n');
for(int i = 0;i < list.size();++i){
QStringList data = list[i].split(" ");
if(data.size() == 4){
vec.push_back(TestStruct(data[0].toInt(),data[1].toInt(),data[2].toInt(),data[3].toInt()));
}else{
vec.clear();
TOAST("测试录制文件不正常.");
break;
}
}
file.close();
}else{
TOAST("打开测试录制文件失败.");
}
LOGOUT<<"testRecord size:"<<vec.size();
return CheckValid();
}
void AutoTestManager::SaveData()
{
if(CheckValid()){
QFile file("/mnt/opt/Qtopia4.8.6/bin/testRecord.dat");
if(file.open(QIODevice::ReadWrite | QIODevice::Truncate)){
QString str;
for(size_t i = 0;i < vec.size();++i){
str += vec[i].ToString();
}
qint64 len = file.write(str.toStdString().c_str(),str.size() - 1);
LOGOUT<<"dataLen:"<<str.size()<<" writeLen:"<<len;
TOAST("保存成功");
file.close();
}else{
TOAST("打开测试录制文件失败.");
}
}
}
bool AutoTestManager::CheckValid()
{
LOGOUT<<"command size:"<<vec.size();
if(vec.size() >= 2 && vec[0].cmd == QEvent::None && vec[vec.size() - 1].cmd == QEvent::None){
size_t i = 0;
for(;i < vec.size();++i){
if(!vec[i].IsValid()){
LOGOUT<<i<<vec[i].Print()<<"is invalid";
break;
}
}
if(i == vec.size()){
TOAST("测试录制文件正常.");
return true;
}
}
LOGOUT<<"测试录制文件不正常.";
TOAST("测试录制文件不正常.");
vec.clear();
return false;
}
void AutoTestManager::Print()
{
LOGOUT<<vec.size();
for(size_t i = 0;i < vec.size();++i){
LOGOUT<<QString::number(i)<<vec[i].Print();
}
}
实现下来还有的问题就是,比如 QComboBox 这样的控件,给它发送一些事件,它是不直接处理的,会有问题,这种针对这些控件单独处理就可以了。