人机交互心理学实践 – 基于Qt的心理学问卷输入系统搭建

本文介绍了一种将纸质问卷数据录入计算机的一种方法,使用基于Qt的GUI程序进行数据录入。根据人因学相关理论,注意瞬脱(AB),工作记忆容量和及时反馈等相关领域的研究,程序尽量避免了手动输入数据的困扰:“注意力转移的精力消耗以及逐个检查错误的烦躁”,使用计算机分担了输入者大部分的工作。本程序提升了1-3倍的输入效率,并且使输入者心情舒畅。因此较为成功的完成了设计目标。

程序设计缘起

为什么我们需要一个帮助我们输入数据的界面程序,而不是选择SPSS、Excel或者是Notepad?这是一个微妙的问题。我们仔细的分析一下场景,比如,要使用TXT文档,我们唯一需要做的就是新建一个文本文档,然后输入一个数字,空格/Tab按键,然后再输入一个数字。Excel/SPSS也是如此,把不同题目的答案输入到不同单元格里,每行数据代表一个被试,这看起来很“逻辑正确”。

能够阻止我们开发一套全新的界面程序来输入系统的理由实在是太多了,尤其是考虑到输入数据这件事情是如此的简单、方便,以至于我们只要有一台带屏幕的电子设备,不论是手机、平板还是笔记本、而或是工作站,我们都可以录数据,更有甚者,我觉得Apple Watch也可以完成录入数据的职责。

关键的问题是,我们很容易忽视,在这么多的,这么方便的录入方式中,我们忽略了什么东西?没错,人。我们忽略了,录入数据的主要参与者其实不是机器,而是人。而这个程序的完成,最重要的就是替代部分的“人”的工作。

作为“录入数据”活动主体的人

作为一个曾经被问卷折磨的快要疯掉的人,为了本科论文——我尝试过两天内录入了580份问卷,每份问卷包含4页单面A4纸,共98个单项选择题,大概有57000个数据需要输入。我当时采用的方法是使用文本文件,让别人在旁边念,我直接输入,我还记得,专门买来用于输入数据的那把薄膜键盘,还没有输完所有数据就已经坏掉了。

在输入问卷的时候,人的工作是最为辛苦的。一般来说,我们会将每个被试的数据分段,然后每段每段输入,这里最为关键的一个问题是,我们需要关注输入正确性问题,也就是需要耗费精力关注这一串数字的输入是否长度符合(有可能多输入或者少输入),以及输入的正确性是否符合(一般心理学问题答案都是在1-7这几个数字上,如果不看键盘的话,有可能我们会把23444输入成14555这个样子)。而一旦数据不符合长度,我们需要逐个删除分段的数据,然后重新输入,或者逐个查找分段数据,找到错误,这个过程是非常耗费精力(虽然并不耗时)的。因此,录入问卷的最大的问题是当出现错误的时候,我们要进行注意力的转移来处理这些错误,这个过程是最痛苦的。

研究认为,当注意力转移的时候,会出现0.5秒左右的注意瞬脱,在这个过程中,我们无法注意到任何东西,过于频繁的经历注意瞬脱,不断的将注意力从纸质问卷转义到电脑显示的我们输入的数字上并进行逐个检查时耗费精力的。正比如我们输入密码的时候,如果输入错误,我们会全部删掉,然后重新输入,这两个过程大概相似,因为修改错误是十分耗费精力的,正因为此,手工输入纸质问卷的效率才一直很低。此外,对于输入串行的问题,这个事情一般当我们稍微瞄一下输入的内容就可以下意识的认识到是否出现了输入串行,这是个次要问题。

程序详情介绍

本程序是根据之前开发的“社会心理学”问卷输入器二次开发的,我删除了“查看上一条数据”的功能,因为在输入大量数据的时候,我们根本不会去返回去进行全部检查。我的程序将不同纸质问卷的页上不同分段都设置了LineEdit组件,对于每个分段,我们需要输入数字,比如232223,之后按下回车,LineEdit组件会自动发射信号对数值进行检查,主要检查其是否为规定数字并且是否符合长度要求,如果符合,则自动唤起KeyEvent跳转到下一个LineEdit继续输入。

每个分段输入完毕后,在CMD里会显示输入的内容,瞥一眼数据可以帮我们注意是否输串行。每次按下Enter按键后,数值会上传到一个列表中,当被试所有数据输入完毕后,这些数值会根据要求的格式转变成字典对象保存到shelve中,每个对象独立保存,当断电时,只会损失这一个被试的数据,这在一定程度上保证了安全性。所有输入的数据都保存在字典中,可以转换成文本导出,使用导出按钮即可。如果需要重新开始,只需要删除cdb数据库文件即可。

这个程序很简单,但是却几乎完美的解决了我们的需求——让输入者更少的耗费精力处理错误,当输入检查没有通过时,系统会自动锁定输入行,然后选中所有数据,输入者甚至不需要看电脑屏幕,因为出现错误的时候会出现弹窗以及声音提示,只需要按下Enter按键并且重输即可,不需要删除,不需要检查,因为每个分段大概都是7个问题,这样做的原因很简单,人的大脑数字工作记忆单元为7,7个数字方便检查和重新输入。当我们感觉输入不对看一眼CMD窗口的时候,因为7个数字的语音线索还没有消退,因此,我们可以在不看原始数据的情况下对输入的结果和我们大脑中残留的我们“读”纸质数据的结果进行精确比较,如果输入错误,只用按下shift+tab按键切换到相应行,按下ctrl+a重新输入即可,数据会进行复写,不用进行其他操作。

实际上,当输入的时候,我们唯一需要做的就是把注意力关注在纸质问卷上,甚至不需要看键盘,更不用看电脑屏幕,计算机会自动帮我们检查数据的长度,这是人工输入最容易出现的问题。而一旦有这个问题,我们也不用像平常那样一个个关注错误进行修改,系统直接选中了错误段的所有数据,只需要我们重新在纸质问卷上定位到相应点进行重新输入,按下Enter进行检查并且切换到下一个分段。

总而言之,这个程序将输入的效率大概提升了2-2.5倍,并且最关键的,输入数据变成了一种很让人高兴的事,而不再是一种痛苦——因为我们不再需要进行注意点的切换。实际上,使用本程序输入数据,更像是一种游戏,有相应规则,有正确和错误,有及时反馈,这是很有成就感的一件事情,实际上,很多时候,当我空闲的时候,我宁愿选择输入数据而不是打守望先锋。我认为我们的程序最大化了输入的效率,不仅表现在时间上,还表现在输入体验上,从这个角度来说,这个程序的设计达到了其目的。

程序源代码

程序需要Python 3.X版本,以及pyqt5模块,程序可以在NT、Unix、Linux、macOS下运行。程序根据GPL v2协议开源,所有基于此代码开发的程序必须开源,否则将面临相关法律责任。

import sys,os,shelve
import PyQt5
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import UI_collection
import traceback,time,random

__version__ = '0.0.2'
__title__ = "创造力问卷录入器[1231]"

class Form(QWidget,UI_collection.Ui_Form):
    def __init__(self,parent=None):
        super(Form,self).__init__(parent)
        self.setupUi(self)
        self.lineEdit_name.setFocus()
        self.dbindex = ((0,"name"),)
        self.currentindex = 0
        index = self.dbindex[-1][0]
        self.currentindex = index + 1
        self.label_info.setText("当前为第%s项"%self.currentindex)

        self.lineEdit_11.returnPressed.connect(self.checkData)
        self.lineEdit_12.returnPressed.connect(self.checkData)
        self.lineEdit_21.returnPressed.connect(self.checkData)
        self.lineEdit_22.returnPressed.connect(self.checkData)
        self.lineEdit_23.returnPressed.connect(self.checkData)
        self.lineEdit_31.returnPressed.connect(self.checkData)
        self.lineEdit_32.returnPressed.connect(self.checkData)
        self.lineEdit_33.returnPressed.connect(self.checkData)
        self.lineEdit_41.returnPressed.connect(self.checkData)
        self.lineEdit_42.returnPressed.connect(self.checkData)
        self.lineEdit_51.returnPressed.connect(self.checkData)
        self.lineEdit_52.returnPressed.connect(self.checkData)
        self.lineEdit_61.returnPressed.connect(self.checkData)
        self.lineEdit_name.returnPressed.connect(self.checkData)
        self.lineEdit_date.returnPressed.connect(self.checkData)
        self.lineEdit_sex.returnPressed.connect(self.checkData)

        self.user = {}
        self.temp = {}
        self.pushButton_n.clicked.connect(self.nextOne)
        self.pushButton_o.clicked.connect(self.outPut)
        # self.pushButton_p.clicked.connect(self.previousOne)

    def checkData(self):
        # print("In")
        objid = None
        if not isinstance(self.sender(),QLineEdit):
            return None
        lineedit_obj = self.sender()
        # print(lineedit_obj,lineedit_obj.text())
        lineedit_text = str(lineedit_obj.text())
        # print(len(lineedit_text))

        if lineedit_obj == self.lineEdit_11:
            objid = 11
            if not lineedit_text.isdigit() or len(lineedit_text) != 8:
                self.showWarn()
                self.lineEdit_11.setFocus()
                self.lineEdit_11.selectAll() 
                return None
                

        elif lineedit_obj == self.lineEdit_12:
            objid = 12
            if not lineedit_text.isdigit() or len(lineedit_text) != 8:
                self.showWarn()
                self.lineEdit_12.setFocus()
                self.lineEdit_12.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_21:
            objid = 21
            if not lineedit_text.isdigit() or len(lineedit_text) != 8:
                self.showWarn()
                self.lineEdit_21.setFocus()
                self.lineEdit_21.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_22:
            objid = 22
            if not lineedit_text.isdigit() or len(lineedit_text) != 8:
                self.showWarn()
                self.lineEdit_22.setFocus()
                self.lineEdit_22.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_23:
            objid = 23
            if not lineedit_text.isdigit() or len(lineedit_text) != 10:
                self.showWarn()
                self.lineEdit_23.setFocus()
                self.lineEdit_23.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_31:
            objid = 31
            if not lineedit_text.isdigit() or len(lineedit_text) != 9:
                self.showWarn()
                self.lineEdit_31.setFocus()
                self.lineEdit_31.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_32:
            objid = 32
            if not lineedit_text.isdigit() or len(lineedit_text) != 10:
                self.showWarn()
                self.lineEdit_32.setFocus()
                self.lineEdit_32.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_33:
            objid = 33
            if not lineedit_text.isdigit() or len(lineedit_text) != 10:
                self.showWarn()
                self.lineEdit_33.setFocus()
                self.lineEdit_33.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_41:
            objid = 41
            if not lineedit_text.isdigit() or len(lineedit_text) != 13:
                self.showWarn()
                self.lineEdit_41.setFocus()
                self.lineEdit_41.selectAll() 
                return None         
                
        elif lineedit_obj == self.lineEdit_42:
            objid = 42
            if not lineedit_text.isdigit() or len(lineedit_text) != 17:
                self.showWarn()
                self.lineEdit_42.setFocus()
                self.lineEdit_42.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_51:
            objid = 51
            if not lineedit_text.isdigit() or len(lineedit_text) != 12:
                self.showWarn()
                self.lineEdit_51.setFocus()
                self.lineEdit_51.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_52:
            objid = 52
            if not lineedit_text.isdigit() or len(lineedit_text) != 13:
                self.showWarn()
                self.lineEdit_52.setFocus()
                self.lineEdit_52.selectAll() 
                return None
                
        elif lineedit_obj == self.lineEdit_61:
            objid = 61
            if not lineedit_text.isdigit() or len(lineedit_text) != 24:
                self.showWarn()
                self.lineEdit_61.setFocus()
                self.lineEdit_61.selectAll() 
                return None                                                                                                                   
                

        elif lineedit_obj == self.lineEdit_name:
            objid = 1
            if len(lineedit_text) == 0:
                self.showWarn()
                self.lineEdit_name.setFocus()
                self.lineEdit_name.selectAll() 
                return None       
                
        elif lineedit_obj == self.lineEdit_date:
            objid = 2
            if len(lineedit_text) != 8:
                self.showWarn()
                self.lineEdit_date.setFocus()
                self.lineEdit_date.selectAll()
                return None         

        elif lineedit_obj == self.lineEdit_sex:
            objid = 3
            if not lineedit_text in ("1","2","3"):
                self.showWarn()
                self.lineEdit_sex.setFocus()
                self.lineEdit_sex.selectAll()
                return None             

        self.goNext(objid,lineedit_text)
        # return lineedit_text

    def goNext(self,objid,text):
        keypress = QKeyEvent(QEvent.KeyPress,Qt.Key_Tab,Qt.NoModifier)
        QCoreApplication.sendEvent(self,keypress)
        print(objid,text)
        self.user[objid] = text


    def showWarn(self):
        QMessageBox.warning(self,"WARN","格式不正确")
        

    def outPut(self):
        try:
            line = "\n%s\n"%time.ctime()
            db = shelve.open("userdb.cdb")
            userdb = db["info"]
            metadata = ("id","name","school","nianji","banji","sex","birth")
            for n in range(32):
                a = n + 1
                metadata += ("a"+str(a),)
            for n in range(19):
                b = n + 1
                metadata += ("b"+str(b),)
            for n in range(33):
                c = n + 1
                metadata += ("c"+str(c),)
            for n in range(17):
                d = n + 1
                metadata += ("d"+str(d),)
            for n in range(12):
                e = n + 1
                metadata += ("e"+str(e),)
            for n in range(37):
                f = n + 1
                metadata += ("f"+str(f),)

            for user in userdb:
                item = userdb[user]
                for n in metadata:
                    line += str(item[n]) + "\t" 
                line += "\n"

            print(line,file=open("result.txt",'w'))
            QMessageBox.information(self,"Output Checker","导出成功,文件在工作目录下的result.txt文件内。")
        except:
            QMessageBox.warning(self,"Output Checker",str(traceback.format_exc()))


    def uploadData(self,kind = "new",dbaddress="",dbname = "userdb.cdb"):
        if kind == "new":
            if len(self.user) != None: #== 16
                try:
                    self.fuser = {}
                    self.fuser["id"] = random.randint(10000,99999)
                    self.fuser["name"] = self.user[1]
                    self.fuser["school"] = "未知"
                    self.fuser["nianji"] = "未知"
                    self.fuser["banji"] = "未知"
                    self.fuser["sex"] = self.user[3]
                    self.fuser["birth"] = self.user[2][:4] + "." + self.user[2][4:6] + "." + self.user[2][6:]
                    a = str(self.user[11]) + str(self.user[12]) + str(self.user[21]) + str(self.user[22])
                    for answer in range(len(a)):
                        answer = answer + 1
                        key = "a" + str(answer)
                        self.fuser[str(key)] = a[answer-1]
                    b = str(self.user[23]) + str(self.user[31])
                    for answer in range(len(b)):
                        answer = answer + 1
                        key = "b" + str(answer)
                        self.fuser[str(key)] = b[answer-1] 
                    c = str(self.user[32]) + str(self.user[33]) + str(self.user[41])
                    for answer in range(len(c)):
                        answer = answer + 1
                        key = "c" + str(answer)
                        self.fuser[str(key)] = c[answer-1]
                    d = str(self.user[42])
                    for answer in range(len(d)):
                        answer = answer + 1
                        key = "d" + str(answer)
                        self.fuser[str(key)] = d[answer-1]
                    e = str(self.user[51])
                    for answer in range(len(e)):
                        answer = answer + 1
                        key = "e" + str(answer)
                        self.fuser[str(key)] = e[answer-1]
                    f = str(self.user[52]) + str(self.user[61])
                    for answer in range(len(f)):
                        answer = answer + 1
                        key = "f" + str(answer)
                        self.fuser[str(key)] = f[answer-1]

                    print(self.fuser)
                    db = shelve.open(dbname)
                    try:   
                        database = db["info"]
                        dataindex = db["index"]
                    except:
                        db["info"] = {}
                        db["index"] = ()
                        database = db["info"]
                        dataindex = db["index"]

                    name = str(self.fuser["name"]) 
                    index = self.dbindex[-1][0]
                    self.dbindex += ((index+1,name),)
                    database[name] = self.fuser
                    db["index"] = self.dbindex
                    db["info"] = database
                    db.close()
                    return 1,"已更新记录"

                except:
                    return 0,"数值不完整"
                
            else:
                return 0,"数值不完整"
            

            

        self.currentindex = index + 1

    def previousOne(self):
        self.updateUI()
        print(self.currentindex)
        if self.currentindex < 2:
            return 0
        else:
            self.currentindex -= 1
        self.label_info.setText("当前为第%s项"%self.currentindex)
        print(self.dbindex)
        
        for item in self.dbindex:
            num,name = item
            if num == self.currentindex:
                break
        db = shelve.open("userdb.cdb")
        try:
            userdb = db["info"]
            for name_store in userdb:
                if name == name_store:
                    self.temp = userdb[name_store]
                    print("I find data from db",self.temp)
        except:
            QMessageBox.warning(self,"WARN",str(traceback.format_exc()))

        self.restoreUI() 


    def nextOne(self):
        code,info = self.uploadData()
        if code == 1:
            self.user = {}
            self.temp = {}
            self.updateUI()
        else:
            QMessageBox.warning(self,"WARN",info)
        
        index = self.dbindex[-1][0]
        self.currentindex = index + 1
        self.label_info.setText("当前为第%s项"%self.currentindex)
        self.lineEdit_name.setFocus()


    def restoreUI(self):
        user = self.user
        self.lineEdit_name.setText(user[1])
        self.lineEdit_sex.setText(user[3])
        self.lineEdit_date.setText(user[2])
        self.lineEdit_11.setText(user[11])
        self.lineEdit_12.setText(user[12])
        self.lineedit_21.setText(user[21])
        self.lineEdit_22.setText(user[22])
        self.lineEdit_23.setText(user[23])
        self.lineEdit_31.setText(user[31])
        self.lineEdit_32.setText(user[32])
        self.lineEdit_33.setText(user[33])
        self.lineEdit_41.setText(user[41])
        self.lineEdit_42.setText(user[42])
        self.lineEdit_51.setText(user[51])
        self.lineEdit_52.setText(user[52])
        self.lineEdit_61.setText(user[61])

    
    def contextMenuEvent(self, event):
        menu = QMenu()
        helpAction = menu.addAction("如何使用(&H)")
        helpAction.triggered.connect(self.showHelp)
        menu.addSeparator()
        aboutmeAction  = menu.addAction("关于此软件(&A)")
        aboutmeAction.triggered.connect(self.aboutMe)
        # menu.addSeparator()
        aboutmeAction2  = menu.addAction("关于Qt(&Q)")
        aboutmeAction2.triggered.connect(self.aboutMe2)
        
        menu.exec_(event.globalPos())
    

    def updateUI(self,new=False):
        for item in (self.lineEdit_11,self.lineEdit_12,self.lineEdit_21,self.lineEdit_22,
                        self.lineEdit_23,self.lineEdit_31,self.lineEdit_32,self.lineEdit_33,
                        self.lineEdit_41,self.lineEdit_42,self.lineEdit_51,self.lineEdit_52,
                        self.lineEdit_61,self.lineEdit_date,self.lineEdit_name,self.lineEdit_sex):
            item.clear()

    
    def showHelp(self):
        QMessageBox.information(self,"帮助 - A Corkine's Software","""
        <html><head/><body><p>%s version %s</p><p>请联系cm@marvinstudio.cn获取帮助。</p></body></html>"""
        %(__title__,__version__))

    def aboutMe(self):
        QMessageBox.about(self,"ABOUT - A Corkine's Way","""
        <html><head/><body>
        <p>%s version %s</p><p>Written by <span style=" font-weight:600;">Corkine Ma</span></p>
        <p>Email:cm@marvinstudio.cn</p>
        <p>本程序使用 Python 和 Qt 开发,程序遵循GPL v2开源协议,你可以在http://tools.mazhangjing.com 此网站找到程序的源代码,如果没有,请联系作者。 </p>
        </body></html>
        """%(__title__,__version__)
        )

    def aboutMe2(self):
            QMessageBox.aboutQt(self,"Python LOVE Qt & C++")




if __name__=="__main__":
    app = QApplication(sys.argv)
    form = Form()
    form.show()
    app.exec_()