Writing a small voting tool in Qt

published at 17.04.2014 20:41 by Jens Weller

I've just written this afternoon a small tool for the talk-vote for Meeting C++ 2014. As the call for papers ends on Sunday, the voting comes closer. Last year only a few people on the program committee could vote, this year its going to be around 300. This is 10 times more then last year. In this way I want to give the audience a chance to influence the program. I'll get into the voting details in the last paragraph.

For the last weeks I've been using my free time to work on my back end application, which is more or less a database front end. One of its features is to handle the voting, it exports one XML file for each person which is able to vote. Voting is already pretty easy with this XML file, as its pretty printed and only containing the talks + vote and comments. Today I wrote a small tool, which reads this file, displays each talk and lets you vote and leave a comment for each talk:

../../files/blog/mcppvotingtool.png

This tool is written with Qt, and in order to use it, you'll need to have a voting file and compile the tool sources with the Qt SDK. Lets have a quick look at the sources...

The data of the talks is stored a small helper class:

class TalkData
{
    QString id,title,desc,comment;
    int vote;
public:
    TalkData(const QString& id, const QString& title,const QString& desc, const QString& comment, int vote);
/* setter + getter */
};

The talks are getting loaded from the XML file into a vector of TalkData objects. The whole purpose of the tool is to cycle through the talks and do the voting. All the data is managed by the class Talks:

class Talks
{
    typedef std::vector talk_container;
    talk_container talks;
    talk_container::size_type pos=0;
    QString voteid;
public:
    bool loadTalkData(const QString& path);
    bool saveTalkData(const QString& path);

    const TalkData& begin()const;
    const TalkData& current()const;
    void update( const QString& comment, int vote);
    talk_container::size_type prev();
    talk_container::size_type next();
    talk_container::size_type size()const{return talks.size();}
    talk_container::size_type getPos()const{return pos;}
private:
    void pushTalk(const QString& id, const QString& title, const QString& desc, const QString& comment, int vote);
};

The member variable pos holds the current index of the talk which is displayed in the tool. Update sets the data of comment and vote for the current element at talks[pos]. The loadTalkData method uses one of my utility classes for loading data from an XML Stream: XMLTokenReader

XMLTokenReader tokenreader;
tokenreader.registerTagHandler("talk", XMLTokenReader::handler_t([&id](QXmlStreamReader& reader)
    {
        QXmlStreamAttributes attrs = reader.attributes();
        auto it = std::find_if(attrs.begin(),attrs.end(),[](QXmlStreamAttribute& attr){return attr.name() =="id";});
        if(it != attrs.end())
            id = it->value().toString();
    }));
//more handlers for elements like title, description etc.
tokenreader.registerEndTagHandler("talk", XMLTokenReader::handler_t([&id,&title,&desc,&comment,&vote,this](QXmlStreamReader& reader) { pushTalk(id,title,desc,comment,vote); vote = 1; id.clear(); title.clear(); desc.clear(); comment.clear(); })); QFile file(path); if(!file.open(QIODevice::ReadOnly)) return false; QTextStream stream(&file); QString xml = stream.readAll(); tokenreader.read(xml);

XMLTokenReader reads from a QXMLStreamReader internally the xml tags, for each tag there can be a handler registered. Those could be methods of a helper class, or in this case more convenient C++11 lambdas. Handlers can be defined for the opening or the closing of a tag. The handler gets the reader it self to execute the necessary actions for the current tag.

The mainform constructor simply loads the data from the selected voting file and adds the radiobuttons to a QButtonGroup:

btngroup.addButton(ui->rb_vote0,0);
btngroup.addButton(ui->rb_vote1,1);
btngroup.addButton(ui->rb_vote2,2);
btngroup.addButton(ui->rb_vote3,3);
btngroup.addButton(ui->rb_vote4,4);
btngroup.addButton(ui->rb_vote5,5);

//lets start with loading the talks directly...
on_actionLoad_Voting_XML_File_triggered();

ui is the pointer to the form class generated by the moc containing the variables for the ui form. The main form class VotingTool is basically only a bunch of glue code. It loads the data from TalkData into the form:

void VotingTool::update(const TalkData &talkdata)
{//transfer data into the controls
    ui->lbl_title->setText(QString("Talk %1: %2").arg(talks.getPos()+1 ).arg(talkdata.getTitle()));
    ui->txt_desc->setText(talkdata.getDesc());
    ui->txt_comment->setPlainText(talkdata.getComment());
    int vote = talkdata.getVote();
    assert(vote > -1 && vote < 6);
    btngroup.button(vote)->setChecked(true);
    ui->btn_next->setEnabled(true);
}

The code for saving the data back into TalkData class is very similar, but only dealing with possible comments and the vote. The only interesting part of the code left is saving the data back into a vote file. This happens again in the Talks class:

bool Talks::saveTalkData(const QString &path)
{
    if(talks.empty())
        return false;
    QFile file(path);
    if(!file.open(QIODevice::WriteOnly))
        return false;
    QXmlStreamWriter xml(&file);
    xml.setAutoFormatting(true);
    xml.writeStartDocument();
    xml.writeStartElement("SubmittedTalks");
    xml.writeTextElement("voteid",voteid);

    for(auto&& item: talks)
    {
        xml.writeStartElement("talk");
        xml.writeAttribute("id",item.getId());

        xml.writeTextElement("title",item.getTitle());
        xml.writeTextElement("desc",item.getDesc());
        xml.writeTextElement("vote",QString::number(item.getVote()));
        xml.writeTextElement("comment",item.getComment());

        xml.writeEndElement();
    }
    xml.writeEndElement();
    xml.writeEndDocument();
    return true;
}

This code simply uses QXmlStreamWriter to save the data into the correct XML format.

The voting

This tool has a single purpose: to enable people to easily vote on the submitted talks for Meeting C++ 2014. Like I wrote earlier, around 300 people will receive an XML voting file per email, which they can load into the tool. Each talk can be rated between 0 and 5 points, optional you might also leave a comment. But the vote is far more important then the comment. As each talk is rated by the sum of its votes, the vote decides which talks could make it to the conference. The saved voting results file will be imported into my own back end, which is already prepared to handle the vote import. And also the tool source can show you how easy it is to write small tools with Qt.

Update 18th April

I've uploaded a new version featuring first(<<) and last(>>) buttons, this makes navigation easier. Also the font for the talk title is now a bit bigger, and I added support for arrow keys for navigating the talks. Currently there are 37 talks submitted, most likely it will be over 40 at sunday. Voting starts monday. Download is the same.

Update 20th April

Further improvements, I added voting with 0-5 keys and this version can now also read the json format. You will receive tomorrow both xml and json files for voting. Now there are 46 talks, voting will provide me the necessary hints to shape this years conference program!