I have several custom widget in my current project. I wish to apply stylesheets to them and when I do so inside Qt Creator, it appears to work. However, when executing the program, no stylesheet is used. The stylesheets for the Qt widgets are working normally.
Does anyone have any advice?
WidgetUnits.h
#ifndef WIDGETUNITS_H
#define WIDGETUNITS_H
#include <QList>
#include <QWidget>
#include <QPainter>
#include <Widgets/JECButton.h>
#include <Unit.h>
#include <Time.h>
namespace Ui
{
class WidgetUnits;
}
class WidgetUnits : public QWidget
{
Q_OBJECT
public:
explicit WidgetUnits(QWidget *parent = 0);
~WidgetUnits();
void setNumTimes(const int& numTimes);
public slots:
void updatePictures(const Time* time);
protected:
void paintEvent(QPaintEvent *event);
private:
void checkNewQueue(const QList<QList<Unit*>*>* units);
Ui::WidgetUnits *ui;
const int pictureWidth; // The width of the Unit pictures.
const int pictureHeight; // The height of the Unit pictures.
QList<QList<JECButton*>*> buttonPictures; // The Units' pictures. The outer QList stores the QList of pictures for a given tick.
// The inner QList stores the JECButtons for the specific tick.
};
WidgetUnits.cpp
#include "WidgetUnits.h"
#include "ui_WidgetUnits.h"
WidgetUnits::WidgetUnits(QWidget *parent):
QWidget(parent),
ui(new Ui::WidgetUnits),
pictureWidth(36),
pictureHeight(36)
{
ui->setupUi(this);
}
WidgetUnits::~WidgetUnits()
{
delete ui;
}
void WidgetUnits::updatePictures(const Time *time)
{
// Only showing units that started to get built this turn.
checkNewQueue(time->getUnits());
checkNewQueue(time->getBuildings());
checkNewQueue(time->getUpgrades());
// Updating the position of the remaining pictures (after some were removed).
// Checking the maximum number of Units made in one tick.
int maxNewQueue = 0;
for (int a = 0; a < buttonPictures.length(); ++a)
{
if (buttonPictures.at(a)->length() > maxNewQueue)
{
maxNewQueue = buttonPictures.at(a)->length();
}
}
if (buttonPictures.length() > 0)
{
this->setGeometry(0, 0, buttonPictures.length() * 130,
maxNewQueue * (pictureWidth + 10) + 20);
QList<JECButton*>* tickButtons = 0;
for (int a = 0; a < buttonPictures.length(); ++a)
{
tickButtons = buttonPictures.at(a);
for (int b = 0; b < tickButtons->length(); ++b)
{
tickButtons->at(b)->move(a * 130, b * (pictureHeight + 10));
}
}
}
update();
}
void WidgetUnits::checkNewQueue(const QList<QList<Unit *> *> *units)
{
if (units != 0)
{
const Unit* currentUnit = 0;
JECButton* currentButton = 0;
for (int a = 0; a < units->length(); ++a)
{
buttonPictures.append(new QList<JECButton*>());
for (int b = 0; b < units->at(a)->length(); ++b)
{
currentUnit = units->at(a)->at(b);
// Verifying that there is an item in the queue and the queue action was started this turn.
if (currentUnit->getQueue() != 0 && currentUnit->getAction()->getTimeStart() == currentUnit->getAction()->getTimeCurrent()
&& (currentUnit->getAction()->getType() == Action::BUILD || currentUnit->getAction()->getType() == Action::TRAIN ||
currentUnit->getAction()->getType() == Action::UPGRADE))
{
buttonPictures.last()->append(new JECButton(this));
currentButton = buttonPictures.last()->last();
QImage* image = new QImage(currentUnit->getQueue()->getUnitBase()->getImage().scaled(pictureWidth, pictureHeight));
currentButton->setImage(*image);
currentButton->setGeometry(0, 0, currentButton->getImage().width(),
currentButton->getImage().height());
currentButton->setColorHover(QColor(0, 0, 225));
currentButton->setColorPressed(QColor(120, 120, 120));
currentButton->setImageOwner(true);
currentButton->setVisible(true);
}
}
}
}
}
void WidgetUnits::setNumTimes(const int &numTimes)
{
// Appending new button lists for added ticks.
for (int a = buttonPictures.length(); a < numTimes; ++a)
{
buttonPictures.append(new QList<JECButton*>());
}
}
void WidgetUnits::paintEvent(QPaintEvent *event)
{
QWidget::paintEvent(event);
}
The widget is visible- I set a tooltip which it showed me (It's just the same color of the QScrollArea it's sitting in).
I had a similar problem and it was solved using jecjackal's comment. As sjwarner said, it would be much more noticeable in the form of an answer. So I'll provide it. For the benefit of any future viewers. Again, it isn't my answer! Appreciate jecjackal for it!
As it is said in the Qt's stylesheets reference, applying CSS styles to custom widgets inherited from QWidget requires reimplementing paintEvent() in that way:
void CustomWidget::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
Without doing it your custom widgets will support only the background, background-clip and background-origin properties.
You can read about it here: Qt Stylesheets reference in the section "List of Stylable Widgets" -> QWidget.
There is an answer much easier than writing your own paintEvent: subclass QFrame instead of QWidget and it will work right away:
class WidgetUnits : public QFrame
{
Q_OBJECT
....
For completeness, the same problem is present in PyQt. You can apply a stylesheet to a subclassed QWidget by adding similar code:
def paintEvent(self, pe):
opt = QtGui.QStyleOption()
opt.init(self)
p = QtGui.QPainter(self)
s = self.style()
s.drawPrimitive(QtGui.QStyle.PE_Widget, opt, p, self)
I had same problem with pyside. I post my solution just for completeness.
It is almost like in PyQt as Pieter-Jan Busschaert proposed.
only difference is you need to call initFrom instead of init
def paintEvent(self, evt):
super(FreeDockWidget,self).paintEvent(evt)
opt = QtGui.QStyleOption()
opt.initFrom(self)
p = QtGui.QPainter(self)
s = self.style()
s.drawPrimitive(QtGui.QStyle.PE_Widget, opt, p, self)
One other thing you need to make sure is that you define your custom widget in your css file the following way:
FreeDockWidget{...}
and not like often recommended
QDockWidget#FreeDockWidget{...}
Calling setAttribute(Qt::WA_StyledBackground, true) for the custom widget worked for me.
Setting Qt::WA_StyledBackground to true only works if you remember to add Q_OBJECT to your class. With these two changes you don't need to reimplement paintEvent.
Related
I need to append text to QPlainTextEdit without adding a newline to the text, but both methods appendPlainText() and appendHtml() adds actually new paragraph.
I can do that manually with QTextCursor:
QTextCursor text_cursor = QTextCursor(my_plain_text_edit->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertText("string to append. ");
That works, but I also need to keep scroll at bottom if it was at bottom before append.
I tried to copy logic from Qt's sources, but I stuck on it, because there actually QPlainTextEditPrivate class is used, and I can't find the way to do the same without it: say, I don't see method verticalOffset() in QPlainTextEdit.
Actually, these sources contain many weird (at the first look, at least) things, and I have no idea how to implement this.
Here's the source code of append(): http://code.qt.io/cgit/qt/qt.git/tree/src/gui/widgets/qplaintextedit.cpp#n2763
I'll just quote what I found here:
http://www.jcjc-dev.com/2013/03/qt-48-appending-text-to-qtextedit.html
We just need to move the cursor to the end of the contents in the QTextEdit and use insertPlainText. In my code, it looks like this:
myTextEdit->moveCursor (QTextCursor::End);
myTextEdit->insertPlainText (myString);
myTextEdit->moveCursor (QTextCursor::End);
As simple as that. If your application needs to keep the cursor where it was before appending the text, you can use the QTextCursor::position() and QTextCursor::setPosition() methods, or
just copying the cursor before modifying its position [QTextCursor QTextEdit::textCursor()] and then setting that as the cursor [void QTextEdit::setTextCursor(const QTextCursor & cursor)].
Here’s an example:
QTextCursor prev_cursor = myTextEdit->textCursor();
myTextEdit->moveCursor (QTextCursor::End);
myTextEdit->insertPlainText (myString);
myTextEdit->setTextCursor (&prev_cursor);
The current Answer was not an option for me. It was much simplier to add html with no new lines with the following method.
//logs is a QPlainTextEdit object
ui.logs->moveCursor(QTextCursor::End);
ui.logs->textCursor().insertHtml(out);
ui.logs->moveCursor(QTextCursor::End);
Ok, I'm not sure if my solution is actually "nice", but it seems to work for me: I just made new class QPlainTextEdit_My inherited from QPlainTextEdit, and added new methods appendPlainTextNoNL(), appendHtmlNoNL(), insertNL().
Please NOTE: read comments about params check_nl and check_br carefully, this is important! I spent several hours to figure out why is my widget so slow when I append text without new paragraphs.
/******************************************************************************************
* INCLUDED FILES
*****************************************************************************************/
#include "qplaintextedit_my.h"
#include <QScrollBar>
#include <QTextCursor>
#include <QStringList>
#include <QRegExp>
/******************************************************************************************
* CONSTRUCTOR, DESTRUCTOR
*****************************************************************************************/
QPlainTextEdit_My::QPlainTextEdit_My(QWidget *parent) :
QPlainTextEdit(parent)
{
}
QPlainTextEdit_My::QPlainTextEdit_My(const QString &text, QWidget *parent) :
QPlainTextEdit(text, parent)
{
}
/******************************************************************************************
* METHODS
*****************************************************************************************/
/* private */
/* protected */
/* public */
/**
* append html without adding new line (new paragraph)
*
* #param html html text to append
* #param check_nl if true, then text will be splitted by \n char,
* and each substring will be added as separate QTextBlock.
* NOTE: this important: if you set this to false,
* then you should append new blocks manually (say, by calling appendNL() )
* because one huge block will significantly slow down your widget.
*/
void QPlainTextEdit_My::appendPlainTextNoNL(const QString &text, bool check_nl)
{
QScrollBar *p_scroll_bar = this->verticalScrollBar();
bool bool_at_bottom = (p_scroll_bar->value() == p_scroll_bar->maximum());
if (!check_nl){
QTextCursor text_cursor = QTextCursor(this->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertText(text);
} else {
QTextCursor text_cursor = QTextCursor(this->document());
text_cursor.beginEditBlock();
text_cursor.movePosition(QTextCursor::End);
QStringList string_list = text.split('\n');
for (int i = 0; i < string_list.size(); i++){
text_cursor.insertText(string_list.at(i));
if ((i + 1) < string_list.size()){
text_cursor.insertBlock();
}
}
text_cursor.endEditBlock();
}
if (bool_at_bottom){
p_scroll_bar->setValue(p_scroll_bar->maximum());
}
}
/**
* append html without adding new line (new paragraph)
*
* #param html html text to append
* #param check_br if true, then text will be splitted by "<br>" tag,
* and each substring will be added as separate QTextBlock.
* NOTE: this important: if you set this to false,
* then you should append new blocks manually (say, by calling appendNL() )
* because one huge block will significantly slow down your widget.
*/
void QPlainTextEdit_My::appendHtmlNoNL(const QString &html, bool check_br)
{
QScrollBar *p_scroll_bar = this->verticalScrollBar();
bool bool_at_bottom = (p_scroll_bar->value() == p_scroll_bar->maximum());
if (!check_br){
QTextCursor text_cursor = QTextCursor(this->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertHtml(html);
} else {
QTextCursor text_cursor = QTextCursor(this->document());
text_cursor.beginEditBlock();
text_cursor.movePosition(QTextCursor::End);
QStringList string_list = html.split(QRegExp("\\<br\\s*\\/?\\>", Qt::CaseInsensitive));
for (int i = 0; i < string_list.size(); i++){
text_cursor.insertHtml( string_list.at(i) );
if ((i + 1) < string_list.size()){
text_cursor.insertBlock();
}
}
text_cursor.endEditBlock();
}
if (bool_at_bottom){
p_scroll_bar->setValue(p_scroll_bar->maximum());
}
}
/**
* Just insert new QTextBlock to the text.
* (in fact, adds new paragraph)
*/
void QPlainTextEdit_My::insertNL()
{
QScrollBar *p_scroll_bar = this->verticalScrollBar();
bool bool_at_bottom = (p_scroll_bar->value() == p_scroll_bar->maximum());
QTextCursor text_cursor = QTextCursor(this->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertBlock();
if (bool_at_bottom){
p_scroll_bar->setValue(p_scroll_bar->maximum());
}
}
I'm confused because in original code there are much more complicated calculations of atBottom:
const bool atBottom = q->isVisible()
&& (control->blockBoundingRect(document->lastBlock()).bottom() - verticalOffset()
<= viewport->rect().bottom());
and needScroll:
if (atBottom) {
const bool needScroll = !centerOnScroll
|| control->blockBoundingRect(document->lastBlock()).bottom() - verticalOffset()
> viewport->rect().bottom();
if (needScroll)
vbar->setValue(vbar->maximum());
}
But my easy solution seems to work too.
Like any string:
QTextEdit *myTextEdit = ui->textEdit;
myTextEdit->moveCursor (QTextCursor::End);
myTextEdit->insertPlainText (myString+"\n");
I tried it and it worked.
Introduction:
For education purpose I developed a Java class that enables students to load Tensorflow models in the Tensorflow SavedModel format and use them for classification purpose in Java. For example, they can create a model online with Google's Teachable Machine, download that and use that model right in Java. This also works with many image classification models on tfhub.dev. Thereby I tried to use the new but not well documented Java API and not the deprecated old libtensorflow-API (when I understood everything correctly). As I use BlueJ for that, everything is based on pure Java code linking the required libraries directly in BlueJ's preferences after downloading them. The documentation in the Java code shows where to download the libraries.
Note: I know that "the normal way today" is using Gradle or Maven or sth. but students do not work with these tools. Another note: In the following I only use a few Code excerpts in order to simplify everything to fit into this minimum example.
Problem:
The results of all my loaded models in Java are OK but not that performant as in Python resp. the online demonstrations linked on the Tensorflow website, mainly in Jupyter notebooks. So there seems to be one step wrong in my code.
As a representative test I will now compare the performance of the MoveNet model when using Python and Java. The MoveNet model "Thunder" detects 17 keypoints of a body in an image with 256x256 pixels. I will use exactly the same image (the same file without touching and resizing it) in both setups (I uploaded it to my webspace; this step was done when updating this text, however there are no differences in the results).
Python:
The MoveNet Model comes with a nice online Python demo in a Jupyter notebook:
https://www.tensorflow.org/hub/tutorials/movenet
The code can be found here (Note: I linked to the same image as in my Java project by uploading it to my webspace and linking to it) and the classification result of the image looks like this:
Java:
My Java-based approach ends up in an image like this:
I think that it is not bad, but it isn't perfect. With other models like e.g. Google's imagenet_mobilenet model I get similar results that are ok, but I suppose they are always a bit better when running online demos in Jupyter notebooks. I do not have more evidence - only a feeling. Im some cases the same image from the online demo is recognized as a different class - but not always. I might provide more data on that later.
Assumption and work done yet:
There might be an error in the data structures or algorithms on them in my Java code. I really searched the web for some weeks now, but I am unsure if my code really is precise, mainly as there are too few examples out there. E.g., I tried to change the order of RGB or the way it is calculated in the method that converts an image into a ND array. However, I saw no significant changes. Maybe the error is anywhere else. However, probably it is just as it is. If my code works well and is correct, that it is also ok for me - but I am still wondering why there are differences. Thanks for answers!
Code:
Here is a fully working example with two classes (I know, the Frame with the Panel drawing is bad - I coded this just fast for this example)
/**
* 1. TensorFlow Core API Library: org.tensorflow -> tensorflow-core-api
* https://mvnrepository.com/artifact/org.tensorflow/tensorflow-core-api
* -> tensorflow-core-api-0.4.0.jar
*
* 2. additionally click "View All" and open:
* https://repo1.maven.org/maven2/org/tensorflow/tensorflow-core-api/0.4.0/
* Download the correct native library for your OS
* -> tensorflow-core-api-0.4.0-macosx-x86_64.jar
* -> tensorflow-core-api-0.4.0-windows-x86_64.jar
* -> tensorflow-core-api-0.4.0-linux-x86_64.jar
*
* 3. TensorFlow Framework Library: org.tensorflow -> tensorflow-framework
* https://mvnrepository.com/artifact/org.tensorflow/tensorflow-framework/0.4.0
* -> tensorflow-framework-0.4.0.jar
*
* 4. Protocol Buffers [Core]: com.google.protobuf -> protobuf-java
* https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java
* -> protobuf-java-4.0.0-rc-2.jar
*
* 5. JavaCPP: org.bytedeco -> javacpp
* https://mvnrepository.com/artifact/org.bytedeco/javacpp
* -> javacpp-1.5.7.jar
*
* 6. TensorFlow NdArray Library: org.tensorflow -> ndarray
* https://mvnrepository.com/artifact/org.tensorflow/ndarray
* -> ndarray-0.3.3.jar
*/
import org.tensorflow.SavedModelBundle;
import org.tensorflow.Tensor;
import org.tensorflow.ndarray.IntNdArray;
import org.tensorflow.ndarray.NdArrays;
import org.tensorflow.ndarray.Shape;
import org.tensorflow.types.TInt32;
import java.util.HashMap;
import java.util.Map;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.io.File;
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.BorderLayout;
public class MoveNetDemo {
private SavedModelBundle model;
private String inputLayerName;
private String outputLayerName;
private String keyName;
private BufferedImage image;
private float[][] output;
private int width;
private int height;
public MoveNetDemo(String pFoldername, int pImageWidth, int pImageHeight) {
width = pImageWidth;
height = pImageHeight;
model = SavedModelBundle.load(pFoldername, "serve");
// Read input and output layer names from file
inputLayerName = model.signatures().get(0).getInputs().keySet().toString();
outputLayerName = model.signatures().get(0).getOutputs().keySet().toString();
inputLayerName = inputLayerName.substring(1, inputLayerName.length()-1);
outputLayerName = outputLayerName.substring(1, outputLayerName.length()-1);
keyName = model.signatures().get(0).key();
}
// not necessary here
public String getModelInformation() {
String infos = "";
for (int i=0; i<model.signatures().size(); i++) {
infos += model.signatures().get(i).toString();
}
return infos;
}
public void setData(String pFilename) {
image = null;
try {
image = ImageIO.read(new File(pFilename));
}
catch (Exception e) {
}
}
public BufferedImage getData() {
return image;
}
private IntNdArray fillIntNdArray(IntNdArray pMatrix, BufferedImage pImage) {
try {
int w = pImage.getWidth();
int h = pImage.getHeight();
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
Color mycolor = new Color(pImage.getRGB(j, i));
int red = mycolor.getRed();
int green = mycolor.getGreen();
int blue = mycolor.getBlue();
pMatrix.setInt(red, 0, j, i, 0);
pMatrix.setInt(green, 0, j, i, 1);
pMatrix.setInt(blue, 0, j, i, 2);
}
}
}
catch (Exception e) {
}
return pMatrix;
}
public void run() {
Map<String, Tensor> feed_dict = null;
IntNdArray input_matrix = NdArrays.ofInts(Shape.of(1, width, height, 3));
input_matrix = fillIntNdArray(input_matrix, image);
Tensor input_tensor = TInt32.tensorOf(input_matrix);
feed_dict = new HashMap<>();
feed_dict.put(inputLayerName, input_tensor);
Map<String, Tensor> res = model.function(keyName).call(feed_dict);
Tensor output_tensor = res.get(outputLayerName);
output = new float[17][3];
for (int i= 0; i<17; i++) {
output[i][0] = output_tensor.asRawTensor().data().asFloats().getFloat(i*3)*256;
output[i][1] = output_tensor.asRawTensor().data().asFloats().getFloat(i*3+1)*256;
output[i][2] = output_tensor.asRawTensor().data().asFloats().getFloat(i*3+2);
}
}
public float[][] getOutputArray() {
return output;
}
public static void main(String[] args) {
MoveNetDemo im = new MoveNetDemo("/Users/myname/Downloads/Code/TF_Test_04_NEW/movenet_singlepose_thunder_4", 256, 256);
im.setData("/Users/myname/Downloads/Code/TF_Test_04_NEW/test.jpeg");
JFrame jf = new JFrame("TEST");
jf.setSize(300, 300);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ImagePanel ip = new ImagePanel(im.getData());
jf.add(ip, BorderLayout.CENTER);
JButton st = new JButton("RUN");
st.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
im.run();
ip.update(im.getOutputArray());
}
});
jf.add(st, BorderLayout.NORTH);
jf.setVisible(true);
}
}
and the ImagePanel class:
import javax.swing.JPanel;
import java.awt.image.BufferedImage;
import java.awt.Graphics;
import java.awt.Color;
public class ImagePanel extends JPanel {
private BufferedImage image;
private float[][] points;
public ImagePanel(BufferedImage pImage) {
image = pImage;
}
public void update(float[][] pPoints) {
points = pPoints;
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0,0,null);
g.setColor(Color.GREEN);
if (points != null) {
for (int j=0; j<17; j++) {
g.fillOval((int)points[j][0], (int)points[j][1], 5, 5);
}
}
}
}
I found the answer. I mixed up height and width twice! No idea, why this behaves so strange (nearly correct but not perfect) but it works now.
In the Jupyter notebook it says:
input_image: A [1, height, width, 3]
so I changed the method fillIntArray to:
private IntNdArray fillIntNdArray(IntNdArray pMatrix, BufferedImage pImage) {
try {
int w = pImage.getWidth();
int h = pImage.getHeight();
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
Color mycolor = new Color(pImage.getRGB(j, i));
int red = mycolor.getRed();
int green = mycolor.getGreen();
int blue = mycolor.getBlue();
pMatrix.setInt(red, 0, i, j, 0); // switched j and i
pMatrix.setInt(green, 0, i, j, 1); // switched j and i
pMatrix.setInt(blue, 0, i, j, 2); // switched j and i
}
}
}
catch (Exception e) {
}
return pMatrix;
}
and accordingly in the run()-method:
IntNdArray input_matrix = NdArrays.ofInts(Shape.of(1, height, width, 3));
In the Jupyter notebook you can toggle the helper functions for visualization and see that at first y and then x coordinates are taken. Height first, then width. Changing this in the ImagePanel class too, solves the problem and the classification is as expected and the same quality as in the online demonstration!
if (points != null) {
for (int j=0; j<17; j++) {
// switched 0 and 1
g.fillOval((int)points[j][1], (int)points[j][0], 5, 5);
}
}
Here it is:
I have a hierarchical data structure of 3 structs in C++, which I renamed in my minimum working example (mwe) House, Room, and Objects. Room is a boost::optional type and has a std::vector<Object> member containing all Objects in this room. The Objects are just a container for some numbers.
I am aware that this is overly complex for such information, but it is necessary in the original code and cannot be changed easily. I tried to change it into a std::experimental::optional since we do not use c++17, but this broke some parts in our c++ code and I don't know whether it would actually solve the problem.
In C++, I have no issues at all and the boost::optional and all member variables work perfectly. But after binding everything I run into the weird problem that the std::vector objects is emptied as soon as I access any member variables of the Room. This can be either the length, width, area or the objects itself as shown in the Python example. If the objects are accessed for the first time, they are actually returned normally, but when trying the second access they are gone as if the had been moved. The same behavior applies if you perform x = myHouse.kitchen.objects.copy(). The list is in x and can even be accessed multiple times, but the information in kitchen is immediately lost. Weirdly, this also only applies to the objects and all other double members can be accessed indefinitely.
For the convenience of compiling everything for the mwe and debugging, I stuffed everything into a single cpp file, which is obviously not the case in the original code. For the mwe, the c++ code is compiled and included via cppimport, but also manual compilation does not change anything.
Here is the mwe:
mwe.cpp:
#include <boost/optional.hpp>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <pybind11/complex.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
/* implementing structs */
struct Object {
Object() = default;
Object(double price, double height) : price(price), height(height) {
};
double price = 0.0;
double height = 0.0;
};
struct Room {
Room() = default;
double length;
double width;
std::vector<Object> objects; // this is the buggy vector
double area() const {
return length * width;
};
};
struct House {
House() = default;
boost::optional<Room> bedroom;
boost::optional<Room> kitchen;
boost::optional<Room> livingroom;
std::map<std::string, std::vector<Object>> getObjects() {
std::map<std::string, std::vector<Object>> out;
if (bedroom) {
out.insert(std::make_pair("bedroom", bedroom->objects));
}
if (kitchen) {
out.insert(std::make_pair("kitchen", kitchen->objects));
}
if (livingroom) {
out.insert(std::make_pair("livingroom", livingroom->objects));
}
return out;
};
};
/* everything works fine in C++ -> get data this way to have complete object map */
House initSomethingInCpp() {
auto myHouse = House();
myHouse.bedroom = Room();
myHouse.kitchen = Room();
myHouse.bedroom->length = 10.0;
myHouse.bedroom->width = 2.0;
myHouse.kitchen->length = 5.0;
myHouse.kitchen->width = 3.0;
std::vector<Object> bedroomObjects;
std::vector<Object> kitchenObjects;
Object closet = Object(100.0, 2.5);
Object bed = Object(200.0, 1.0);
Object oven = Object(500.0, 1.5);
Object table = Object(50.0, 1.5);
bedroomObjects.push_back(closet);
bedroomObjects.push_back(bed);
kitchenObjects.push_back(oven);
kitchenObjects.push_back(table);
myHouse.bedroom->objects = bedroomObjects;
myHouse.kitchen->objects = kitchenObjects;
return myHouse;
};
namespace pybind11 {
/* taking care of boost type */
namespace detail {
/* boost::optional */
template<typename T>
struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> {};
} // namespace detail
} // namespace pybind11
/* binding structs */
void init_house(pybind11::module& main) {
pybind11::class_<House> house(main, "House");
house.def(pybind11::init<>());
house.def_readwrite("bedroom", &House::bedroom);
house.def_readwrite("kitchen", &House::kitchen);
house.def_readwrite("livingroom", &House::livingroom);
house.def("get_objects", &House::getObjects);
};
void init_room(pybind11::module& main) {
pybind11::class_<Room> room(main, "Room");
room.def(pybind11::init<>());
room.def_readwrite("length", &Room::length);
room.def_readwrite("width", &Room::width);
room.def_readwrite("objects", &Room::objects);
room.def_property_readonly("area", &Room::area);
};
void init_objects(pybind11::module& main) {
pybind11::class_<Object> object(main, "Object");
object.def(pybind11::init<>());
object.def(pybind11::init<double, double>());
object.def_readonly("price", &Object::price);
object.def_readonly("heigth", &Object::height);
};
/* define module and bind init_in_cpp function */
PYBIND11_MODULE(mwe, m) {
init_house(m);
init_room(m);
init_objects(m);
m.def("init_something_in_cpp", &initSomethingInCpp);
};
execute.py:
import cppimport
#cppimport.set_quiet(False)
#cppimport.force_rebuild()
mod = cppimport.imp('mwe')
# get data
myHouse = mod.init_something_in_cpp()
print("\n")
print("all data is here")
objs = myHouse.get_objects()
print(objs)
print(myHouse.kitchen.area) # by accessing kitchen members, the objects list is emptied
print("\n")
print("kitchen objects are now missing")
objs = myHouse.get_objects()
print(objs)
print("but area still works:")
print(myHouse.kitchen.area) # everything but objects still works
print("\n")
print("also works directly with same variable")
print("bedroom objects are accessed:")
print(myHouse.bedroom.objects)
print("bedroom objects are accessed again:")
print(myHouse.bedroom.objects)
The execution gives the following output:
all data is here
{'bedroom': [mwe.Object object at 0x7fbc9c2a43f0, mwe.Object object at 0x7fbc9c2a4670], 'kitchen': [mwe.Object object at 0x7fbc9c2a4c30, mwe.Object object at 0x7fbc9c2a4df0]}
15.0
kitchen objects are now missing
{'bedroom': [mwe.Object object at 0x7fbc9c2a4e70, mwe.Object object at 0x7fbc9c2a4eb0], 'kitchen': []}
but area still works:
15.0
also works directly with same variable
bedroom objects are accessed:
[mwe.Object object at 0x7fbc9c2a4c30, mwe.Object object at 0x7fbc9c2a4df0]
bedroom objects are accessed again:
[]
Turns out this was actually a bug in pybind11 release 2.5 https://github.com/pybind/pybind11/issues/1919
It is fixed in the current master branch and future releases.
I've seen this page: https://docs.python.org/3/c-api/object.html but there doesn't seem to be any way to call functions like long_lshift or long_or.
It's not essential to me to call these functions, I could also live with the more generic versions, although I'd prefer to call these. Anyways, is there any way to use these? What do I need to include? Below is some example code, where I'd like to use them (simplified):
size_t parse_varint(parse_state* state) {
int64_t value[2] = { 0, 0 };
size_t parsed = parse_varint_impl(state, value);
PyObject* low = PyLong_FromLong(value[0]);
PyObject* high;
if (value[1] > 0) {
high = PyLong_FromLong(value[1]);
PyObject* shift = PyLong_FromLong(64L);
PyObject* high_shifted = long_lshift(high, shift);
state->out = long_or(low, high_shifted);
} else {
state->out = low;
}
PyObject_Print(state->out, stdout, 0);
return 0;
}
I couldn't find these functions in documentation, but they appear to be exported in Python.h header:
PyNumber_Lshift is the replacement for long_shift in my code.
Similarly, PyNumber_Or is the replacement for long_or in my code.
I'm extending my library with Python (2.7) by wrapping interfaces with SWIG 2.0, and have a graph object in which I want to create a visitor. In C++, the interface looks like this:
struct Visitor
{
virtual void OnStateBegin() = 0;
virtual void OnNode(Node* n) = 0;
virtual void OnStateEnd() = 0;
};
I would like to define a class in Python that does the equivalent, all defined in python, that will allow for the definition of a visitor:
class GraphVisitor:
def __init__(self, label):
self._label = label
print("__init__(self,{0})".format(self._label))
def OnStateBegin(self):
print("OnStateBegin()" + self._label)
def OnNode(self, i_node):
print("OnNode()" + self._label)
def OnStateEnd(self):
print("OnStateEnd()" + self._label)
And what I'm trying to do is create an instance of a GraphVisitor in python script, and call the methods OnStateBegin(), OnNode(), and OnStateEnd() for a given instance from C++. Here's what I'd like to do in Python:
#model is a SWIG wrapped class
mvis = GraphVisitor("This is a test")
model.Visit("mvis") # I'm not sure how to pass the instance 'mvis' to C++?
And in my C++ wrapped by Swig, I'm not sure how to get at the instance 'mvis'? I can call functions defined in Python no problem, but instances has me stumped!
In order to solve this problem, I retrieved the class from the module given it's module name and class name (the code below assumes the module hasn't already been loaded):
void Model::Visit(const char* mod_name, const char* class_name)
{
PyErr_Clear();
PyObject* mod_name_obj = PyString_FromString(mod_name);
PyObject* class_name_obj = PyString_FromString(class_name);
PyObject* py_module = PyImport_Import(mod_name_obj);
PyObject* err_1 = PyErr_Occurred();
if(err_1)
PyErr_Print();
Once I had the module, I looked up the class from it's dictionary:
if(py_module)
{
PyObject* py_module_dict = PyModule_GetDict(py_module);
PyObject* py_class = PyDict_GetItem(py_module_dict, class_name_obj);
I simplified my problem a bit by instantiating the python class in C++, then created my visitor, and finally visited it:
if(py_class && PyClass_Check(py_class) && PyCallable_Check(py_class))
{
PyObject* inst = PyInstance_New(py_class, 0, 0);
if(inst && PyInstance_Check(inst))
{
IModel::IVisitorPtr py_visitor = new PyModelVisitor(inst);
_model->Visit(py_visitor);
}
}
}
}
The visitor had 3 functions OnStateBegin(), OnNode(), and OnStateEnd(). I added to my SWIG python binding generator an option to generate a header file for external access to the SWIG runtime with the -external-runtime option, so I could create a class in C++ (INode* below) and pass it to Python as the argument to the python OnNode() member function as follows (error checking removed for brevity):
VisitorCtrl OnNode(INode* node)
{
Node* node_impl = new NodeImpl(node);
PyObject* pynode = SWIG_NewPointerObj(node_impl, SWIG_TypeQuery("Node *"), 0);
PyObject* result = PyObject_CallMethodObjArgs(_inst, PyString_FromString("OnNode"), pynode, 0);
long rivis = PyInt_AsLong(result);
return(static_cast<VisitorCtrl>(rivis));
}
I don't know if that's possible with SWIG, but you can do it with SIP.
sip_vector_test.h:
class EXPORT Node {
public:
explicit Node(int n) : n_(n) {};
int getN() const { return n_; }
private:
int n_;
};
struct EXPORT NodeVisitor {
virtual void OnNode(Node* n) = 0;
};
struct EXPORT Graph {
public:
void addNode(int num);
void accept(NodeVisitor *nv);
private:
std::vector< std::shared_ptr<Node> > nodes_;
};
visitor.sip:
%Module pyvisit
%ModuleHeaderCode
#include "sip_visitor_test.h"
%End
class Node {
public:
explicit Node(int n);
int getN() const;
};
struct NodeVisitor {
virtual void OnNode(Node* n) = 0;
};
struct Graph {
public:
void addNode(int num);
void accept(NodeVisitor *nv);
};
Using it from Python:
>>> import pyvisit
>>> g = pyvisit.Graph()
>>> g.addNode(3)
>>> g.addNode(5)
>>> class PyNodeVisitor(pyvisit.NodeVisitor):
>>> def OnNode(self, node):
>>> print(node.getN())
>>> pnv = PyNodeVisitor()
>>> g.accept(pnv)
3
5
I've put a zip file containing the source code of this test project on my homepage.