25
\$\begingroup\$

I'm developing applications using Qt which highly make usage of the JSON language to communicate, store and load data of different types. I often need a simple viewer similar to the Firebug JSON explorer to view this data. I already had a JSON parser and serializer called QJson. (update: I also posted my QJson class on Code Review.)

I think this code snippet might be of public interest and there are some open problems (see below) which might be solved by you. I for myself would be glad if the problems get solved or the code will be improved in another point, but I don't need it really. So it is up to you (of course) if you want to review / test / improve the code and contribute.

Features / Preview

QJsonView is a QWidget so you can embed it in any other widget. You can set the value as a JSON-serialized string or as a hierarchical QVariant. It performs syntax-highlighting using HTML and displays this HTML using a QLabel.

An example usage might look like this:

QString data = "{"
                   "\"test\" :  [\"this\", \"is\", \"a\", "
                                 "{\"test\" : [\"with\", \"nested\", \"items\"]}],"
                   "\"types\" : [1337, 13.37, true, null]"
               "}";
QJsonView *jsonView = new QJsonView(this);
jsonView->setJsonValue(data);

Preview: Initial view
Preview: Initial view

You can expand a JSON object or array (QVariantMap or QVariantList respectively) by clicking on the [+] sign. This can also be done from within your code. The entries then are displayed one below another and can be expanded again if they are objects or arrays. Expanding a nested element from within your code is currently not supported.

jsonView->expand();

By enabling hover effects, QJsonView highlights the entry the mouse is currently over:

jsonView->setHoverEffects(true);

Preview: Expanded view with hover effects
Preview: Expanded view with hover effects

From within the context menu, the user can copy the JSON-serialized representation into the clipboard. If this is performed on a string, it doesn't get serialized but copied 1:1.

Preview: Context menu: copy to clipboard
Preview: Context menu: copy to clipboard

When shown as a stand-alone QWidget, it looks like this (here: fully expanded):

Preview: Windowed, fully expanded
Preview: Windowed, fully expanded

Known problems / Possible improvements

The following things are not implemented well, but I didn't have the time and/or motivation to do it better:

  • The font family is set to monospaced by QJsonView.
  • The custom paint event expects a fixed font size.
  • The spacing expects a fixed font size.
  • When using hover effects, the palette gets manipulated and reset to the palette of the parent widget. Two problems occur: (1) If there is no such parent widget, boom. (2) If you assign a custom palette, it will be reset to the parent palette when the mouse leaves the widget.

qjsonview.h:

#ifndef QJSONVIEW_H
#define QJSONVIEW_H

#include <QWidget>
#include <QVariant>
#include <QLabel>

/**
  Widget to display JSON or QVariant data.
  This widget will display any JSON-encoded string or a hierarchically nested QVariant tree in an expandable way.
  Per default, the whole data gets displayed in one single (non-wrapped) line, which can be expanded using a button
  if the JSON / QVariant data is of type JSON-array (QVariantList) or JSON-object (QVariantMap).
*/
class QJsonView : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(bool hoverEffects READ hoverEffects WRITE setHoverEffects);
    Q_PROPERTY(bool expandable READ isExpandable);
    Q_PROPERTY(bool expanded READ isExpanded WRITE setExpanded);


public:
    /**
      Constructor for QJsonView, taking the parent widget as a single argument.
    */
    explicit QJsonView(QWidget *parent = 0);

    /**
      Static and public helper function returning the HTML code which will be used to visualize the data (by applying syntax highlighting rules).
      This function is kept public since you may want to use this to layout some other QVariant data the same way like QJsonView does.
    */
    static QString variantToHtml(QVariant data);


signals:
    /**
      Emitted whenever this widget or one of its children has been expanded or collapsed.
      (The signal gets propagated to the root QJsonView object.)
    */
    void resized();


public slots:
    /**
      Set the value to be displayed to a QVariant value. The only supported QVariant-types are Invalid, Bool, Int, LongLong, List, Map. Any other types are untested!
    */
    void setValue(QVariant value);

    /**
      Set the value to be displayed to a JSON serialized string, which will be decoded before being viewed.
    */
    void setJsonValue(QString json);

    /**
      Enables or disables hover effects.
    */
    void setHoverEffects(bool enabled = true);

    /**
      Returns true if hover effects are enabled.
    */
    bool hoverEffects();

    /**
      Returns true if this QJsonView is expandable.
      This is the case for JSON-objects and JSON-arrays having at least one entry.
    */
    bool isExpandable();

    /**
      Returns true if this QJsonView is currently expanded.
    */
    bool isExpanded();

    /**
      Expands or collapses this view (convenient slot for expand() or collapse(), depending on the argument).
    */
    void setExpanded(bool expanded);

    /**
      Expands this view if it is expandable and not expanded.
    */
    void expand();

    /**
      Collapses this view if it is expanded.
    */
    void collapse();


protected:
    /**
      \reimp
    */
    void mousePressEvent(QMouseEvent *);
    /**
      \reimp
    */
    void paintEvent(QPaintEvent *);
    /**
      \reimp
    */
    void contextMenuEvent(QContextMenuEvent *);
    /**
      \reimp
    */
    void enterEvent(QEvent *);
    /**
      \reimp
    */
    void leaveEvent(QEvent *);

    /**
      Called by a child in order to inform this widget that the mouse cursor is now over the child instead of this widget.
    */
    void childEntered();

    /**
      Called by a child in order to inform this widget that the mouse cursor isn't over the child anymore.
    */
    void childLeaved();




private:
    // value to be displayed, as a QVariant
    QVariant v;
    // if this is no container type, this points to the QLabel representing the single value
    QLabel *lblSingle;
    // if this is a container type, these point to child widgets
    QList<QWidget*> childWidgets;
    // true if this is a container type and is currently in expanded view
    bool expanded;
    // true if hover effects are enabled
    bool hoverEffectsEnabled;

    // apply hover effect
    void hover();
    // revert hover effect
    void unhover();
};

#endif // QJSONVIEW_H

qjsonview.cpp:

#include "qjsonview.h"
#include "qjson.h"
#include <QGridLayout>
#include <QPainter>
#include <QVariantMap>
#include <QContextMenuEvent>
#include <QMenu>
#include <QClipboard>
#include <QApplication>
#include <QMouseEvent>
#include <QTextDocument>
#include <QDebug>
#include <QToolTip>


#define EXPANDABLE_MARGIN_LEFT 14
#define EXPANDED_MARGIN_LEFT 21


QJsonView::QJsonView(QWidget *parent) :
    QWidget(parent),
    lblSingle(new QLabel(this)),
    expanded(false),
    hoverEffectsEnabled(false)
{
    //needed for hover effects
    setAutoFillBackground(true);

    QGridLayout *layout = new QGridLayout;
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(0);
    setLayout(layout);

    //default: show one single QLabel with the whole value as its content
    layout->addWidget(lblSingle);
    lblSingle->setAutoFillBackground(true);
    lblSingle->setCursor(Qt::ArrowCursor);
    setValue(QVariant());
}

void QJsonView::setValue(QVariant value)
{
    if(expanded) collapse();

    v = value;
    lblSingle->setText(QString("<span style=\"font-family: monospace; overflow: hidden\">%1</span>")
                       .arg(variantToHtml(v)));
    layout()->setContentsMargins(isExpandable() ? EXPANDABLE_MARGIN_LEFT : 0, 0, 0, 0);

    //show hand cursor if expandable
    Qt::CursorShape cursor;
    if(isExpandable())
        cursor = Qt::PointingHandCursor;
    else
        cursor = Qt::ArrowCursor;
    setCursor(cursor);
    lblSingle->setCursor(cursor);

    update();
    emit resized();
}

void QJsonView::setJsonValue(QString json)
{
    setValue(QJson::decode(json));
}

void QJsonView::setHoverEffects(bool enabled)
{
    hoverEffectsEnabled = enabled;
    if(!hoverEffectsEnabled)
        unhover();
}

bool QJsonView::hoverEffects()
{
    //if my parent is also a QJsonView, return its property
    QJsonView *p = qobject_cast<QJsonView*>(parentWidget());
    if(p)
        return p->hoverEffects();
    else
        return hoverEffectsEnabled;
}

QString QJsonView::variantToHtml(QVariant data)
{
    if(data.type() == QVariant::String || data.type() == QVariant::ByteArray)
        return "<span style=\"color: #006000\">\"" + Qt::escape(data.toString()) + "\"</span>";
    else if(data.type() == QVariant::Int || data.type() == QVariant::LongLong)
        return "<span style=\"color: #800000\">" + Qt::escape(data.toString()) + "</span>";
    else if(data.type() == QVariant::Double)
        return "<span style=\"color: #800080\">" + Qt::escape(data.toString()) + "</span>";
    else if(data.type() == QVariant::Bool || data.isNull() || !data.isValid())
    {
        QString str = "null";
        if(data.type() == QVariant::Bool)
            str = data.toBool() ? "true" : "false";
        return "<span style=\"color: #000080\">" + str + "</span>";
    }

    else if(data.type() == QVariant::List)
    {
        QString str = "<span style=\"color: #606060\"><b>[</b></span>";
        bool first = true;
        foreach(QVariant e, data.toList())
        {
            if(!first)
                str += "<span style=\"color: #606060\"><b>, </b></span>";
            first = false;
            str += variantToHtml(e);
        }
        str += "<span style=\"color: #606060\"><b>]</b></span>";
        return str;
    }

    else if(data.type() == QVariant::Map)
    {
        QString str = "<span style=\"color: #606060\"><b>{</b></span>";
        QVariantMap map(data.toMap());

        //special entry: "children" => tree view
        bool containsChildren = false;
        QVariant children;
        if(map.contains("children")) {
            children = map.take("children");
            containsChildren = true;
        }

        //normal entries
        QVariantMap::iterator i;
        for(i = map.begin(); i != map.end(); ++i)
        {
            if(i != map.begin())
                str += "<span style=\"color: #606060\"><b>, </b></span>";
            str += Qt::escape(i.key()) + ": " + variantToHtml(i.value());
        }
        //entry "children"
        if(containsChildren) {
            if(!map.isEmpty())
                str += "<span style=\"color: #606060\"><b>, </b></span>";
            str += "children: " + variantToHtml(children);
        }

        str += "<span style=\"color: #606060\"><b>}</b></span>";
        return str;
    }

    else
        return data.toString();
}

void QJsonView::paintEvent(QPaintEvent *)
{
    QPainter p(this);

    // i designed the graphics using a pixel font size of 15, so this should be scalable now.
    qreal scale = fontMetrics().height() / 15.0;
    p.scale(scale, scale);

    int h = height() / scale;

    p.drawRect(2, 2, 10, 10);
    p.drawLine(5, 7, 9, 7);
    if(!expanded)
        p.drawLine(7, 5, 7, 9);

    if(expanded)
    {
        QColor color(96, 96, 96);
        if(v.type() == QVariant::List)
        {
            p.fillRect(16, 2, 4, 1, color);
            p.fillRect(16, 3, 2, h - 6, color);
            p.fillRect(16, h - 3, 4, 1, color);
        }
        else
        {
            int mid = h / 2;
            p.fillRect(18, 2, 4, 1, color);
            p.fillRect(17, 3, 2, mid - 4, color);
            p.fillRect(16, mid - 1, 3, 1, color);
            p.fillRect(15, mid    , 3, 1, color);
            p.fillRect(16, mid + 1, 3, 1, color);
            p.fillRect(17, mid + 2, 2, h - mid - 5, color);
            p.fillRect(18, h - 3, 4, 1, color);
        }
    }
}

void QJsonView::mousePressEvent(QMouseEvent *e)
{
    if(isExpandable()
            && e->button() == Qt::LeftButton
            && (!expanded || e->x() < EXPANDED_MARGIN_LEFT))
    {
        if(!expanded)
            expand();
        else
            collapse();
    }
}

void QJsonView::contextMenuEvent(QContextMenuEvent *e)
{
    QMenu menu(this);

    //copy value to clipboard
    QAction *copy;
    if(v.type() == QVariant::List || v.type() == QVariant::Map)
        copy = menu.addAction(tr("Copy value (JSON encoded)"));
    else if(v.type() == QVariant::String || v.type() == QVariant::ByteArray)
        copy = menu.addAction(tr("Copy string value"));
    else
        copy = menu.addAction(tr("Copy value"));

    //execute menu
    QAction *triggeredAction = menu.exec(e->globalPos());

    if(triggeredAction == copy)
    {
        QClipboard *clipboard = QApplication::clipboard();
        if(v.type() == QVariant::List || v.type() == QVariant::Map || v.type() == QVariant::Bool || v.isNull() || !v.isValid())
            clipboard->setText(QJson::encode(v, QJson::EncodeOptions(QJson::Compact | QJson::EncodeUnknownTypesAsNull)));
        else
            clipboard->setText(v.toString());
    }
}

void QJsonView::enterEvent(QEvent *)
{
    hover();

    //if my parent is also a QJsonView, i inform it that i have been entered
    QJsonView *p = qobject_cast<QJsonView*>(parentWidget());
    if(p) p->childEntered();
}

void QJsonView::leaveEvent(QEvent *)
{
    unhover();

    //if my parent is also a QJsonView, i inform it that i have been leaved
    QJsonView *p = qobject_cast<QJsonView*>(parentWidget());
    if(p) p->childLeaved();
}

bool QJsonView::isExpandable()
{
    return (v.type() == QVariant::List && !v.toList().isEmpty()) ||
            (v.type() == QVariant::Map && !v.toMap().isEmpty());
}

bool QJsonView::isExpanded()
{
    return expanded;
}

void QJsonView::setExpanded(bool expanded)
{
    if(expanded)
        expand();
    else
        collapse();
}

void QJsonView::expand()
{
    if(isExpandable())
    {
        lblSingle->setVisible(false);
        layout()->removeWidget(lblSingle);

        if(v.type() == QVariant::List)
        {
            foreach(QVariant e, v.toList())
            {
                QJsonView *w = new QJsonView(this);
                w->setValue(e);
                layout()->addWidget(w);
                childWidgets << w;
                //propagate signals to parent
                connect(w, SIGNAL(resized()), SIGNAL(resized()));
            }
        }
        else if(v.type() == QVariant::Map)
        {
            QVariantMap map(v.toMap());

            //normal entries
            QVariantMap::iterator i;
            int index = 0;
            QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
            sizePolicy.setHorizontalStretch(0);
            sizePolicy.setVerticalStretch(0);
            for(i = map.begin(); i != map.end(); ++i)
            {
                QLabel *k = new QLabel(this);
                k->setText("<span style=\"font-family: monospace\">" + Qt::escape(i.key()) + ": </span>");
                k->setAlignment(Qt::AlignTop | Qt::AlignLeft);
                k->setCursor(Qt::ArrowCursor);
                k->setAutoFillBackground(true);
                ((QGridLayout*)layout())->addWidget(k, index, 0);
                childWidgets << k;

                QJsonView *w = new QJsonView(this);
                w->setValue(i.value());
                ((QGridLayout*)layout())->addWidget(w, index, 1);
                w->setSizePolicy(sizePolicy);
                childWidgets << w;
                //propagate signals to parent
                connect(w, SIGNAL(resized()), SIGNAL(resized()));

                index++;
            }
        }

        layout()->setContentsMargins(EXPANDED_MARGIN_LEFT, 0, 0, 0);
        expanded = true;
        update();
        emit resized();
    }
}

void QJsonView::collapse()
{
    if(isExpandable())
    {
        foreach(QWidget *w, childWidgets)
        {
            w->deleteLater();
            layout()->removeWidget(w);
        }
        childWidgets.clear();

        lblSingle->setVisible(true);
        layout()->addWidget(lblSingle);

        layout()->setContentsMargins(isExpandable() ? EXPANDABLE_MARGIN_LEFT : 0, 0, 0, 0);
        expanded = false;
        update();
        emit resized();
    }
}

void QJsonView::childEntered()
{
    unhover();
}

void QJsonView::childLeaved()
{
    hover();
}

void QJsonView::hover()
{
    if(hoverEffects())
    {
        QPalette pal = palette();
        pal.setColor(backgroundRole(), Qt::white);
        setPalette(pal);
    }
}

void QJsonView::unhover()
{
    setPalette(parentWidget()->palette());
}
\$\endgroup\$
  • \$\begingroup\$ With Qt5 , QJson module are embedded inside the framework. I made a treemodel from Qt model view perspective. github.com/dridk/QJsonmodel \$\endgroup\$ – dridk Jan 23 '15 at 21:10
  • \$\begingroup\$ JSON isn't a language :) \$\endgroup\$ – Reinderien Aug 1 '15 at 5:58
  • 1
    \$\begingroup\$ What version to c++ do you plan to use for your code? \$\endgroup\$ – fghj Jun 9 '16 at 21:26
2
\$\begingroup\$

I only see minor suggestions so far.

bool hoverEffects();

should be

bool hoverEffects() const;

and likewise for isExpandable, isExpanded.

Depending on your version of C++, there is some syntactic sugar to simplify your map iterator in variantToHtml; do some reading about auto.

Otherwise I don't see anything glaring.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

I see a few other minor things:

This code:

if(data.type() == QVariant::String || data.type() == QVariant::ByteArray)
    return "<span style=\"color: #006000\">\"" + Qt::escape(data.toString()) + "\"</span>";
else if(data.type() == QVariant::Int || data.type() == QVariant::LongLong)
    return "<span style=\"color: #800000\">" + Qt::escape(data.toString()) + "</span>";
else if(data.type() == QVariant::Double)
    return "<span style=\"color: #800080\">" + Qt::escape(data.toString()) + "</span>";
else if(data.type() == QVariant::Bool || data.isNull() || !data.isValid())

should be a switch(data.type()) making use of a default: section. (Split off QVariant::Bool; it's categorically different than null and invalid)

However, this code has a lot of repetition. The only differences between the three THEN clauses is the color and whether or not they're quoted. They all start and end the same. Maybe use some string formatting with a string similar to "<span style=\"color: %1\">%2%3%2</span>"

if(!first)
     ...
first = false;

Try not to put conditionals inside the loop. This makes it harder for the processor to pipeline. Instead, go ahead and put the wrong info in as a suffix then delete or fix it up the final one after the loop closes. (change the ", " to "]", easy since you know right where it is) Again this should use the format string from before to reduce the amount of duplicated data.

if(i != map.begin())

Same thing, add the string as a suffix then delete the extra one at the end.

| improve this answer | |
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.