The
RInside
package makes it pretty simple and straightforward to embed
R, the wonderful statistical
programming environment and language, inside of a C++ application. This uses
both the robust embedding API provided by
R itself, and the higher-level
abstractions from our
Rcpp package.
A number of examples are shown on this blog both
here and
here
as well as on the
RInside page;
and the source package actually contains well over a dozen complete examples which cover
anything from simple examples to parallel use via
MPI for parallel computing.
Beginning users sometimes ask about how to use
RInside inside
larger projects. And as I had meant to experiment with embedding inside of
the powerful
Qt framework anyway, I
started to dabble a little. A first result is now in the SVN sources of
RInside.
My starting point was the classic
tkdensity
demo that comes with
R itself. It is a good point of departure as Tcl/Tk makes it very
portable---in fact it should run on every platform that runs
R---and quite
expressive. And having followed some of the GUI experiments around
R over
the years, I have also seen various re-implementations using different GUI frameworks. And so I am adding
mine to this body of work:
The problem I addressed first was actual buildability. For the
RInside
examples, Romain and I provide a Makefile that
just works by making
calls to
R itself to learn about flags
for
R,
Rcpp and
RInside such
that all required headers and libraries are found. That is actually
relatively straightforward (and documented in our vignettes) but a little
intimidating at first---which is why a ready-made Makefile is a good thing.
Qt of course uses
qmake
and the
.pro
files to encode / resolve dependencies. So task one
was to map what our Makefile does into its variables. Turns out that wasn't all that
hard:
## -*- mode: Makefile; c-indent-level: 4; c-basic-offset: 4; tab-width: 8; -*-
##
## Qt usage example for RInside, inspired by the standard 'density
## sliders' example for other GUI toolkits
##
## Copyright (C) 2011 Dirk Eddelbuettel and Romain Francois
TEMPLATE = app
HEADERS = qtdensity.h
SOURCES = qtdensity.cpp main.cpp
QT += svg
## comment this out if you need a different version of R,
## and set set R_HOME accordingly as an environment variable
R_HOME = $$system(R RHOME)
## include headers and libraries for R
RCPPFLAGS = $$system($$R_HOME/bin/R CMD config --cppflags)
RLDFLAGS = $$system($$R_HOME/bin/R CMD config --ldflags)
RBLAS = $$system($$R_HOME/bin/R CMD config BLAS_LIBS)
RLAPACK = $$system($$R_HOME/bin/R CMD config LAPACK_LIBS)
## if you need to set an rpath to R itself, also uncomment
#RRPATH = -Wl,-rpath,$$R_HOME/lib
## include headers and libraries for Rcpp interface classes
RCPPINCL = $$system($$R_HOME/bin/Rscript -e \'Rcpp:::CxxFlags\(\)\')
RCPPLIBS = $$system($$R_HOME/bin/Rscript -e \'Rcpp:::LdFlags\(\)\')
## for some reason when building with Qt we get this each time
## so we turn unused parameter warnings off
RCPPWARNING = -Wno-unused-parameter
## include headers and libraries for RInside embedding classes
RINSIDEINCL = $$system($$R_HOME/bin/Rscript -e \'RInside:::CxxFlags\(\)\')
RINSIDELIBS = $$system($$R_HOME/bin/Rscript -e \'RInside:::LdFlags\(\)\')
## compiler etc settings used in default make rules
QMAKE_CXXFLAGS += $$RCPPWARNING $$RCPPFLAGS $$RCPPINCL $$RINSIDEINCL
QMAKE_LFLAGS += $$RLDFLAGS $$RBLAS $$RLAPACK $$RCPPLIBS $$RINSIDELIBS
## addition clean targets
QMAKE_CLEAN += qtdensity Makefile
The double dollar signs and escaping of parentheses are a little tedious, but
hey it works and expands the compiler and linker flags such that everything <emjust works="Works">.
The code itself is pretty straightforward too. We instantiate the
RInside object
as well as the main
Qt application
object. We then instantiate a new object of class
QtDensity
that
will launch the main widget; it is given a reference to the
RInside object.
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; tab-width: 8; -*-
//
// Qt usage example for RInside, inspired by the standard 'density
// sliders' example for other GUI toolkits
//
// Copyright (C) 2011 Dirk Eddelbuettel and Romain Francois
#include <QApplication>
#include "qtdensity.h"
int main(int argc, char *argv[])
RInside R(argc, argv); // create an embedded R instance
QApplication app(argc, argv);
QtDensity qtdensity(R);
return app.exec();
The definition of the main object is pretty simple: a few private variables, and a few
functions to interact with the GUI and get values from the radio buttons,
slider or input field---as well as functions to update the chart or re-draw
the random variables.
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; tab-width: 8; -*-
//
// Qt usage example for RInside, inspired by the standard 'density
// sliders' example for other GUI toolkits
//
// Copyright (C) 2011 Dirk Eddelbuettel and Romain Francois
#ifndef QTDENSITY_H
#define QTDENSITY_H
#include <RInside.h>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QSlider>
#include <QSpinBox>
#include <QLabel>
#include <QTemporaryFile>
#include <QSvgWidget>
class QtDensity : public QMainWindow
Q_OBJECT
public:
QtDensity(RInside & R);
private slots:
void getBandwidth(int bw);
void getKernel(int kernel);
void getRandomDataCmd(QString txt);
void runRandomDataCmd(void);
private:
void setupDisplay(void); // standard GUI boilderplate of arranging things
void plot(void); // run a density plot in R and update the
void filterFile(void); // modify the richer SVG produced by R
QSvgWidget *m_svg; // the SVG device
RInside & m_R; // reference to the R instance passed to constructor
QString m_tempfile; // name of file used by R for plots
QString m_svgfile; // another temp file, this time from Qt
int m_bw, m_kernel; // parameters used to estimate the density
QString m_cmd; // random draw command string
;
#endif
Lastly, no big magic in the code either (apart from the standard magic provided
by
RInside). A bit of standard GUI layouting, and
then some functions to pick values from the inputs as well as to compute /
update the output. One issue is worth mentioning. The screenshot and code
show the second version of this little application. I built a first one using
a standard portable network graphics (png) file. That was fine, but not
crisp as png is a pixel format so I went back and
experimented with scalable vector graphics (svg) instead. One can create svg output with
R in a number of ways, one of
which is the
cairoDevice
package by Michael Lawrence (who also wrote
RGtk2 and good
chunks of
Ggobi). Now, it turns out that
Qt displays the so-called
SVG
tiny standard whereas
R creates a fuller SVG format. Some
discussion with Michael reveals that one can modify the svg file suitably (which
is what the function
filterFile
below does) and it all works. Well:
almost. There is a bug (and Michael thinks it is the SVG rendering) in which
the density estimate does not get clipped to the plotting region.
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; tab-width: 8; -*-
//
// Qt usage example for RInside, inspired by the standard 'density
// sliders' example for other GUI toolkits -- this time with SVG
//
// Copyright (C) 2011 Dirk Eddelbuettel and Romain Francois
#include <QtGui>
#include "qtdensity.h"
QtDensity::QtDensity(RInside & R) : m_R(R)
m_bw = 100; // initial bandwidth, will be scaled by 100 so 1.0
m_kernel = 0; // initial kernel: gaussian
m_cmd = "c(rnorm(100,0,1), rnorm(50,5,1))"; // simple mixture
m_R["bw"] = m_bw; // pass bandwidth to R, and have R compute a temp.file name
m_tempfile = QString::fromStdString(Rcpp::as<std::string>(m_R.parseEval("tfile <- tempfile()")));
m_svgfile = QString::fromStdString(Rcpp::as<std::string>(m_R.parseEval("sfile <- tempfile()")));
m_R.parseEvalQ("library(cairoDevice)");
setupDisplay();
void QtDensity::setupDisplay(void)
QWidget *window = new QWidget;
window->setWindowTitle("Qt and RInside demo: density estimation");
QSpinBox *spinBox = new QSpinBox;
QSlider *slider = new QSlider(Qt::Horizontal);
spinBox->setRange(5, 200);
slider->setRange(5, 200);
QObject::connect(spinBox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int)));
QObject::connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int)));
spinBox->setValue(m_bw);
QObject::connect(spinBox, SIGNAL(valueChanged(int)), this, SLOT(getBandwidth(int)));
QLabel *cmdLabel = new QLabel("R command for random data creation");
QLineEdit *cmdEntry = new QLineEdit(m_cmd);
QObject::connect(cmdEntry, SIGNAL(textEdited(QString)), this, SLOT(getRandomDataCmd(QString)));
QObject::connect(cmdEntry, SIGNAL(editingFinished()), this, SLOT(runRandomDataCmd()));
QGroupBox *kernelRadioBox = new QGroupBox("Density Estimation kernel");
QRadioButton *radio1 = new QRadioButton("&Gaussian");
QRadioButton *radio2 = new QRadioButton("&Epanechnikov");
QRadioButton *radio3 = new QRadioButton("&Rectangular");
QRadioButton *radio4 = new QRadioButton("&Triangular");
QRadioButton *radio5 = new QRadioButton("&Cosine");
radio1->setChecked(true);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(radio1);
vbox->addWidget(radio2);
vbox->addWidget(radio3);
vbox->addWidget(radio4);
vbox->addWidget(radio5);
kernelRadioBox->setMinimumSize(260,140);
kernelRadioBox->setMaximumSize(260,140);
kernelRadioBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
kernelRadioBox->setLayout(vbox);
QButtonGroup *kernelGroup = new QButtonGroup;
kernelGroup->addButton(radio1, 0);
kernelGroup->addButton(radio2, 1);
kernelGroup->addButton(radio3, 2);
kernelGroup->addButton(radio4, 3);
kernelGroup->addButton(radio5, 4);
QObject::connect(kernelGroup, SIGNAL(buttonClicked(int)), this, SLOT(getKernel(int)));
m_svg = new QSvgWidget();
runRandomDataCmd(); // also calls plot()
QGroupBox *estimationBox = new QGroupBox("Density estimation bandwidth (scaled by 100)");
QHBoxLayout *spinners = new QHBoxLayout;
spinners->addWidget(spinBox);
spinners->addWidget(slider);
QVBoxLayout *topright = new QVBoxLayout;
topright->addLayout(spinners);
topright->addWidget(cmdLabel);
topright->addWidget(cmdEntry);
estimationBox->setMinimumSize(360,140);
estimationBox->setMaximumSize(360,140);
estimationBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
estimationBox->setLayout(topright);
QHBoxLayout *upperlayout = new QHBoxLayout;
upperlayout->addWidget(kernelRadioBox);
upperlayout->addWidget(estimationBox);
QHBoxLayout *svglayout = new QHBoxLayout;
svglayout->addWidget(m_svg);
QVBoxLayout *outer = new QVBoxLayout;
outer->addLayout(upperlayout);
outer->addLayout(svglayout);
window->setLayout(outer);
window->show();
void QtDensity::plot(void)
const char *kernelstrings[] = "gaussian", "epanechnikov", "rectangular", "triangular", "cosine" ;
m_R["bw"] = m_bw;
m_R["kernel"] = kernelstrings[m_kernel]; // that passes the string to R
std::string cmd1 = "Cairo(width=6,height=6,pointsize=10,surface='svg',filename=tfile); "
"plot(density(y, bw=bw/100, kernel=kernel), xlim=range(y)+c(-2,2), main=\"Kernel: ";
std::string cmd2 = "\"); points(y, rep(0, length(y)), pch=16, col=rgb(0,0,0,1/4)); dev.off()";
std::string cmd = cmd1 + kernelstrings[m_kernel] + cmd2; // stick the selected kernel in the middle
m_R.parseEvalQ(cmd);
filterFile(); // we need to simplify the svg file for display by Qt
m_svg->load(m_svgfile);
void QtDensity::getBandwidth(int bw)
if (bw != m_bw)
m_bw = bw;
plot();
void QtDensity::getKernel(int kernel)
if (kernel != m_kernel)
m_kernel = kernel;
plot();
void QtDensity::getRandomDataCmd(QString txt)
m_cmd = txt;
void QtDensity::runRandomDataCmd(void)
std::string cmd = "y <- " + m_cmd.toStdString();
m_R.parseEvalQ(cmd);
plot(); // after each random draw, update plot with estimate
void QtDensity::filterFile()
// cairoDevice creates richer SVG than Qt can display
// but per Michaele Lawrence, a simple trick is to s/symbol/g/ which we do here
QFile infile(m_tempfile);
infile.open(QFile::ReadOnly);
QFile outfile(m_svgfile);
outfile.open(QFile::WriteOnly QFile::Truncate);
QTextStream in(&infile);
QTextStream out(&outfile);
QRegExp rx1("<symbol");
QRegExp rx2("</symbol");
while (!in.atEnd())
QString line = in.readLine();
line.replace(rx1, "<g"); // so '<symbol' becomes '<g ...'
line.replace(rx2, "</g");// and '</symbol becomes '</g'
out << line << "\n";
infile.close();
outfile.close();
What the little application does is actually somewhat neat for the few
lines. One key features is that the generated data can be specified directly by an
R expression which allows for mixtures
(as shown, and as is the default). With that it easy to see how many points are
needed in the second
hump to make the estimate multi-modal, and
how much of a distance between both centers is needed and so on. Obviously,
the effect of the chosen kernel and bandwidth can also be visualized. And with
the chart the being a support vector graphics display, we can resize and
scale at will and it still looks crisp.
The code (for both the simpler png variant and the svg version shown here) is in the SVN repository for
RInside
and will be in the next release. Special thanks to Michael Lawrence for
patiently working through some svg woes with me over a few emails.
Update: Some typos fixed.
Update 2: Two URLs corrected.