26
\$\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());
}
asked May 17, 2012 at 19:30
\$\endgroup\$
3
  • \$\begingroup\$ With Qt5 , QJson module are embedded inside the framework. I made a treemodel from Qt model view perspective. github.com/dridk/QJsonmodel \$\endgroup\$ Commented Jan 23, 2015 at 21:10
  • \$\begingroup\$ JSON isn't a language :) \$\endgroup\$ Commented Aug 1, 2015 at 5:58
  • 1
    \$\begingroup\$ What version to c++ do you plan to use for your code? \$\endgroup\$ Commented Jun 9, 2016 at 21:26

2 Answers 2

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.

answered Jul 25, 2017 at 18:42
\$\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.

answered Dec 21, 2017 at 18:25
\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.