Creating door signs with Qt for Meeting C++ 2022

published at 19.01.2023 17:44 by Jens Weller
Save to Instapaper Pocket

How Meeting C++ 2022 got a major new, but also simple feature: digital door signs.

This actually started with a telephone call, where I've went through the planned conference details with my contact at the hotel in Berlin. At the end, all looked fine, but he had one more task for me: send me the text for the door to display. This got me curious, and so he explained to me how they would now have a system that was able to show the talk/program as a digital door sign. And that these would also be able to display images and - in a limited way - video. So that made me wonder, if it would be feasible to use the image feature instead of text for the conference. This got me really motivated, as that would look so much better, and have some interesting options to display things for the conference. Part of this motivation went directly into trying to work through my todo list. Door signs would be a nice to have, everything else would need to be off the critical path first.

In the end this resulted in having a digital door sign displayed at every door, showing the current or next talk:

door sign image
door sign book image

Step one was to sketch out what data this program needs, and luckily my Database knows which talks are given by whom and when. Also I can query if there is a known image available. So all this information got generated into a json file and send over the net when it was queried.

Then I've had one Saturday time to write the interesting part of this program, the C++ part with Qt. It is split in 3 parts with a push button to run each of them:

All of this is driven by Qt. QNetworkAccessManager is the class which goes through the various needed GET requests to download all the speaker images and sponsor logos.

But first, the constructor of the main window is where I do some of the general setup. Read in the json file if its already downloaded, and otherwise I run some basic setup code to create directories for later use:

basedir = QApplication::applicationDirPath();
qDebug() << basedir;
if(QFile::exists(basedir + "/schedule.json"))
{
    qDebug() << "reading schedule.json";
    QFile file("./schedule.json");
    if(file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        jsondoc = QJsonDocument::fromJson(file.readAll());
        if(!jsondoc.isEmpty())
            ui->btn_generatedoorimg->setEnabled(true);
    }
}
else
{
    QDir dir;
    if(!dir.exists(basedir +"/sponsors")&&!dir.mkdir(basedir +"/sponsors"))
        qDebug() << "could not create sponsor directory";
    if(!dir.exists(basedir +"/speakerimg"))
        dir.mkdir(basedir +"/speakerimg");
    if(!dir.exists(basedir +"/doorimg"))
        dir.mkdir(basedir +"/doorimg");
    if(!dir.exists(basedir +"/pic"))
        dir.mkdir(basedir +"/pic");
}
connect(&httpdownloader,&HttpDownloader::rawDataAvailable,this,&MainWindow::downloadFinished);

This is all rather simple stuff to do a quick setup.

The first button then is simply what kicks of downloading the schedule info from my website:

void MainWindow::on_btn_downloadscheduleinfo_clicked()
{
  if(networkmanager == nullptr)
  {
      networkmanager = new QNetworkAccessManager(this);
      connect(networkmanager,&QNetworkAccessManager::finished,this,&MainWindow::onDownloadfinished);
  }
  networkmanager->get(QNetworkRequest(url));
  ui->btn_generatedoorimg->setEnabled(false);
}

In the onDownloadfinished handler then the data is written to the file on disk. Since this part of the code only handles one file, its not very flexible, and downloading multiple files concurrently with Qt needs a little more. For this I have a HttpDownloader class, which is repurposed from the RSS tool from Meeting C++:

class HttpDownloader : public QObject
{
    Q_OBJECT
    QNetworkAccessManager manager;
    std::map<QNetworkReply*,QVariant> runningreplies;
public:
    explicit HttpDownloader(QObject *parent = nullptr);
    void downloadSources(std::map<QVariant,QUrl>& sources);
    void downloadSource(const QVariant& id, const QUrl& url);

signals:
    void rawDataAvailable(QByteArray array, QVariant id);
    void downloadFinished();

public slots:
    void finished(QNetworkReply* reply);
};

This class has its own NetworkAccessManager, as it shouldn't expose this to the outside. After all the signals are needed in this class. Its public interface is quite simple: downloadSource demands a QVariant and a QUrl to start a download. You also can give a map of QVariant and QUrl if you have a bunch of things to download at once. The QUrl is easy to understand, but the QVariant is the key to understanding this class. It simply will send a signal out with this QVariant id and the QByteArray coming from the network once the download has finished. Also once all downloads are finished, a signal is emitted. Internally, a map of QNetworkReplys keeps hold of the id you gave with the URL. When the slot for the Download is called with this Argument, a lookup in the map will give the corresponding key.

This class is used from the main window, so lets peak into the MainWindow.h to see which general member variables exist:

class MainWindow : public QMainWindow
{
    Q_OBJECT
    QNetworkAccessManager* networkmanager=nullptr;
    QJsonDocument jsondoc;
    HttpDownloader httpdownloader;
    int downloadcounter = 0;
    std::map<int,QString> id2imgpath;
    QString basedir;
    std::vector gold_logos,gold_silver_logos;
    std::map<QString,std::vector speaker2books;
    void readBookinfo(const QJsonArray& books, const QString &name);

When downloading the files, the id used in the program is a simple int, which will also map to the path, a QString. I'd love to use filesystem here with its path class, but to my knowledge Qt does not have a counter class that represents a path. So its a QString. Also I'm not sure if the gcc version on this machine already has C++17 and fancy things like filesystem. And there is some other containers, like for sponsor images and book data. Mostly this is a simple program.

When the second button is pressed, the json file with the schedule data is searched for speaker and sponsor images, which then are downloaded. This example code covers how this is done for the speakers/talks:

QJsonObject doc = jsondoc.object();
QJsonObject schedule = doc["schedule"].toObject();
auto scanforspeakerimg = [this](const QJsonObject& obj){
    auto it_speakerimg = obj.find("speakerimg");
    auto it_name = obj.find("speakername");
    if(it_speakerimg != obj.end() && it_name != obj.end())
    {
        QString speakerimg = it_speakerimg.value().toString();
        QString name = it_name.value().toString()+speakerimg.right(speakerimg.size() - speakerimg.lastIndexOf('.'));
        QString path = basedir +"/speakerimg/"+name;
        if(!QFile::exists(path))
        {
            id2imgpath.insert(std::make_pair(downloadcounter,path));
            this->httpdownloader.downloadSource(QVariant(downloadcounter++),QUrl(speakerimg));
        }
    }
    auto it_books = obj.find("books");
    if(it_books != obj.end())
    {
        QJsonArray books = it_books->toArray();
        readBookinfo(books,it_name->toString());
    }
};
QJsonArray day1 = schedule["Day1"].toArray();
for(auto v:day1)
{
    QJsonObject talk = v.toObject();
    scanforspeakerimg(talk);
}

This all runs on QJsonDocument and its various classes. There is an object for the schedule which contains each day, which is a QJsonArray. There are then traversed in a simple for loop, calling the lambda "scan for speaker image". This lambda does the downloading of the speaker image, if it doesn't already exist. If the speaker has also a book registered with Meeting C++, its also in the data. The program will generate a separate door image for each book.

Until this moment everything runs in the main thread. For writing the actual images to disk I wanted to delegate this to the QThreadPool, in order not to block the program too long. For this an ImageWriter class was created:

class ImageWriter : public QRunnable
{
    QString imgpath;
    QByteArray imgdata;
public:
    ImageWriter(const QString& imgpath,QByteArray imgdata);
    virtual void run() override;
};

This class simply implements a QRunnable, which will write a single image to disk. After using this program, I've been wondering if doing batches here might have been better.

void ImageWriter::run()
{
    QFile file(imgpath);
    if(file.open(QIODevice::WriteOnly))//binary is default
    {
       file.write(imgdata);
    }else qDebug() << "could not write image" << imgpath;
}

The code executed is kinda easy: a QFile is opened from the path and then the QByteArray is written. This is the famous short and self documenting code. But when looking it up in the Qt Documentation on how to write into files with Qt (I've usually done this with iostreams), the first examples you'll find are for text mode. Knowing that this image data better is written in binary form, I've wondered if there is a QIODevice::Binary, as Text exists. Turns out there is not, but its the default of a QIODevice to write in binary.

The MainWindow has a slot where it forwards the QByteArray together with the filepath to the ImageWriter class, which then is handed over to the global QThreadPool:

void MainWindow::downloadFinished(QByteArray bytes, QVariant id)
{
  QString path = id2imgpath[id.toInt()];
  QThreadPool::globalInstance()->start(new ImageWriter(path,bytes));
}

This works as ImageWriter will destroy itself after running, its setting setAutoDelete to true in its constructor.

With this everything is setup to do the actual painting. Since we are not painting in a window context, I thought that QPainter would also be able to be running inside a QThreadPool in parallel. QPainter does unfortunately not support multi threading, but it was interesting to see the painting artifacts this caused. So I kept the general setup to run this in parallel if I'd be wanting to play around with a C++ library supporting this one day, but run this code sequentially in the actual program:

void DoorImageWriter::run()
{
    QString fileday = talkinfo["day"].toString().replace('.','_');
    QString filetime = talkinfo["time"].toString().replace(':','_');
    QString track = talkinfo["track"].toString();
    QString filename = fileday +"_" + track + "_" + track2room[track] +"_"+ filetime+".jpg";

    QPixmap pixmap(1920,1080);
    paintImage(pixmap,true);
    QImage img = pixmap.toImage();
    img.save("doorimg/"+filename,"JPG");
}

This code just calls a painting function, and converts the pixmap to an QImage to write it to disk. But it also contains another important part of the program: creating the actual output file names. These files will go to the hotel, and they'll need to sort in the correct order, and other then the filename there is not much to go for the person handling these. Except when opening the file some of this also is visible. But that is an extra step you don't want to do for lots of files. So the filename contains the day, track, room and time.

Now on to painting...

void DoorImageWriter::paintImage(QPixmap& img, bool silver)
{
    QString time = talkinfo["time"].toString();
    QString day = talkinfo["day"].toString();
    QString eventname = "Meeting C++ "+QString::number(QDate::currentDate().year());
    QString title = talkinfo["title"].toString();
    QString speakername = talkinfo["speakername"].toString();
    QString track = talkinfo["track"].toString();
    QString trackname = QString("Track ") + track +" "+track2room[track];
    qDebug() << trackname;

    img.fill();

    std::map<QString,QColor> track2color ={{"A",Qt::red},{"B",Qt::blue},{"C",Qt::darkGreen}};
    int h = img.height();
    int w = img.width();
    int half_height = h / 2;
    int half_width = w / 2;
    int quarter_width = w/4;
    int colorbarheight = half_height/4;
    int textoffset = 40;
    int offsety = 40;
    QPainter painter(&img);
    painter.setPen(track2color[track]);
    painter.setBrush(track2color[track]);
    painter.drawRect(half_width,0,half_width,colorbarheight);
    painter.setBrush(Qt::black);
    painter.setPen(Qt::black);
    painter.drawRect(0,0,half_width,half_height);
    painter.drawRect(half_width,colorbarheight,half_width,half_height-colorbarheight);
    painter.setBrush(Qt::white);
    painter.setPen(Qt::white);
    painter.setFont(QFont("Arial Black",30));
    //QFontMetrics fm(painter.font());
    //int pixelwide = fm.boundingRect(eventname).width();
    int pixelheight = 70;//fm.height();
    offsety += pixelheight;
    painter.drawText(textoffset,offsety,eventname);
    painter.setFont(QFont("Arial",25));
    offsety += pixelheight+140;
    painter.drawText(textoffset,offsety,speakername);
    painter.setFont(QFont("Arial",20));
    painter.drawText(half_width+textoffset,colorbarheight+pixelheight,trackname);
    painter.drawText(half_width+textoffset,colorbarheight+pixelheight+pixelheight,time);
    offsety += pixelheight+30;
    QFontMetrics fm(painter.font());
    int pixelwide = fm.boundingRect(title).width();
    painter.drawText(textoffset,offsety,title);
    
QString speakerimgurl = talkinfo["speakerimg"].toString(); QString speakerimgfile = "speakerimg/"+ talkinfo["speakername"].toString()+ speakerimgurl.right(speakerimgurl.size() - (speakerimgurl.lastIndexOf('.'))); QImage speakerimg(speakerimgfile); if(pixelwide > half_width + quarter_width) speakerimg = speakerimg.scaled(300,300); painter.drawImage(half_width + quarter_width,colorbarheight/2,speakerimg); //paint sponsors int imgsize =200; int stepx = w / 6; int uppermidy = half_height + half_height/8 -30; int lowermidy = half_height + half_height/2 + 50; int uppermidx = half_width-imgsize; int startx = uppermidx - (stepx * 2 +50); auto paintSponsors=[startx,uppermidy,stepx,lowermidy](QPainter& painter,const std::vector& logos,size_t row){ int posx = startx; for(size_t i = 0; i < row;i++) { painter.drawImage(posx,uppermidy,logos[i]); posx += stepx; } posx = startx; for(size_t i = row; i < logos.size();i++) { painter.drawImage(posx,lowermidy,logos[i]); posx += stepx; } }; paintSponsors(painter,gold_silver_logos,6); }

This function has a lot to do and first thing is to setup so that the image can be drawn. I did not have the time to implement a complex design, in the end this is mostly text, some rectangles and painting the speaker image, then filling the lower half with the sponsor logos. QFontMetrics helps with knowing how long your text will be in the actual image, and in some cases I chose to either make the speaker image smaller or the talk/book title font smaller so that it still fits without being cut off.

And handling the drawing code was one of the things which I didn't enjoy so much. Its just a lot of doing positioning and trying to keep the code clean. Most of the code worked quickly, but some of the corner cases showed up later when looking through the generated images.

One future thing I'd may want to do with this program is to also generate thumbnails for YouTube with this. Also as this is written just in front of the conference, it had to work with what was available. The door signs were reused to be shown in the projector during the breaks in the main room. This was a nice touch, but an HD file seems not to always scale up nicely to the projection, especially with sponsor logos. I'm thinking about exporting an higher resolution version for this next time.

 

 

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!