0

EDIT: I have read Passing and argument to a slot it's helpful, but doesn't address my issue of passing multiple references to a function I called via a signal-slot.

I'm currently working on a Qt application that essentially is a unit converter. I'm implementing it using QDoubleSpinBoxes as the input and the output. I am running into an issue that i'm looking for help with. The implementation idea is that the user will input a value of whatever they want to convert and it will, upon the user losing focus on the spinbox or hitting enter, populate the other unit type boxes with the answer.

Here is how I currently do it:

// creates a custom spinbox for each option
modifiedSpinbox *FahrenheitDblSpinbox = new modifiedSpinbox();
modifiedSpinbox *CelciusDblSpinbox = new modifiedSpinbox();
modifiedSpinbox *KelvinDblSpinbox = new modifiedSpinbox();
modifiedSpinbox *RankineDblSpinbox = new modifiedSpinbox();

// Creates a signal mapper that allows passing of a parameter
// to the convert functions anytime a number is entered
QSignalMapper *tempMapper = new QSignalMapper(this);

// Connects the spinbox editing complete signal with the mapper
connect(tempMapper, SIGNAL(mapped(int)),
        this,SLOT(on_userInput(int whoSignaled)));
// Connects the mapper with the function that calls the convert class
connect(FahrenheitDblSpinbox, SIGNAL(editingFinished()),
        tempMapper, SLOT(map()));
tempMapper->setMapping(FahrenheitDblSpinbox, 1);

The way I would like to implement this conversion function is to, on user finishing their input into a spinbox, have the code send a signal (editingFinished()) to a slot function which calls a converttools class that has the functions needed to do the actual conversion. The problem that i'm running in to is that I cannot figure out how to pass references for these spinbox objects to my converttools class so that I can set the spinbox values directly. The closest i've come is to use QSignalMapper (seen above) to pass a single int or Qwidget to my slot function, not the objects I want.

I would like some advice as to how to pass multiple references to my custom class after a signal is emitted. I've looked though numerous questions here and still cant seem to figure out how to do this or a better way i'm not seeing.

Thanks!

Community
  • 1
  • 1
  • 1
    You should prefer the [new signal/slot syntax](https://wiki.qt.io/New_Signal_Slot_Syntax). – nwp Mar 07 '17 at 12:57
  • @KubaOber If you're suggesting the lambda solution be used... that would be a painful solution, requiring the writing of 12 lambda connections in total :( – Jonathan Mee Mar 07 '17 at 13:52
  • 1
    I have already looked at that page mine is supposedly a duplicate of and I disagree. The problem with QSignalMapper (as I understand it) is that it only allows you to pass an int, QWdiget, or QObject. As far as I know, I can't pass a doublespinbox using it and even if I could, I need to pass multiple references per function call not just a single one. Maybe this is not the best way to implement what I'm trying to do, but that's the only way I can think of that. – spartan228 Mar 07 '17 at 14:02
  • I apologize, my code is a bit confusing as it shows me trying to pass an int which is partially what I want to do. I'm in the middle of playing around with my code which is what I posted. – spartan228 Mar 07 '17 at 14:05
  • @spartan228 I agree that this is not a duplicate. Your best bet is not going to be `QSignalMapper` cause your signals to it have to be parameterless. Instead you're going to need to write your own QObject which accepts mapping *lambdas* and associates them with object methods. – Jonathan Mee Mar 07 '17 at 14:09
  • @KubaOber Just noticed that you reopened. Thanks. I cooked up an answer that was a bit more demanding that I expected :S – Jonathan Mee Mar 08 '17 at 15:28
  • "I need to pass multiple references per function call not just a single one." Using lambdas, you can pass any number of arguments (as many as your compiler supports, really). And since you're passing a list of objects, you'd be hopefully using the `QObjectList` to carry it. But all of that seems super-cumbersome anyway. Yours is an XY Problem: you're presuming a solution without addressing the real problem first. – Kuba hasn't forgotten Monica Mar 08 '17 at 19:53

2 Answers2

1

What you're looking for is:

  1. Signals to carry the QDoubleSpinBox's assigned temperature
  2. A conversion to a common temperature unit
  3. A Signal to all other QDoubleSpinBoxs to update them
  4. A conversion from common temperature unit to each QDoubleSpinBox's specific temperature unit

QSignalMapper is a bad choice here, because:

This class collects a set of parameterless signals, and re-emits them with integer, string or widget parameters corresponding to the object that sent the signal

So we cannot take in the assigned temperature. Instead lets start with a map<QDoubleSpinBox*, pair<function<double(double)>, function<double(double)>>> which will serve to map from a given QDoubleSpinBox to its "conversion to a common temperature unit" and "conversion from a common temperature unit", respectively.

We'll then build an object around this map looking something like this:

class SlotMapper : public QObject
{
    Q_OBJECT
    map<QDoubleSpinBox*, pair<function<double(double)>, function<double(double)>>> mapping;
public:
    SlotMapper() = default;
    SlotMapper(const map<QDoubleSpinBox*, pair<function<double(double)>, function<double(double)>>> mapping) : mapping(mapping) {};
    AddMapping(QDoubleSpinBox* key, function<double(double)> valueFirst, function<double(double)> valueSecond) { mapping.insert_or_assign(key, make_pair(valueFirst, valueSecond)); }
    void map(const double assignedTemperature) const {
        const auto commonTemperatureUnit = mapping.at(QObject()::sender).first(assignedTemperature);

        for(auto it = cbegin(mapping); it != cend(mapping); ++it) {
            if(it->first != QObject()::sender) {
                it->first->blockSignals(true);
                it->first->setValue(it->second.second(commonTemperatureUnit));
                it->first->blockSignals(false);
            }
        }
    }
};

This object should be constructed with all necessary conversion functions. in your case that probably looks something like:

SlotMapper mySlotMapper(map<QDoubleSpinBox*, pair<function<double(double)>, function<double(double)>>>{ {FahrenheitDblSpinbox, make_pair([](const double param){ return (param - 32.0) * 5.0 / 9.0; }, [](const double param){ return param * 9.0 / 5.0 + 32.0; })},
                                                                                                        {CelciusDblSpinbox, make_pair([](const double param){ return param; }, [](const double param){ return param; })},
                                                                                                        {KelvinDblSpinbox, make_pair([](const double param){ return param - 273.15; }, [](const double param){ return param + 273.15; })},
                                                                                                        {RankineDblSpinbox, make_pair([](const double param){ return (param - 491.67) * 5.0 / 9.0; }, [](const double param){ return (param + 273.15) * 9.0 / 5.0; })} });

As far as your connections, they'll look like:

connect(FahrenheitDblSpinbox, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), &mySlotMapper, &SlotMapper::map);
connect(CelciusDblSpinbox, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), &mySlotMapper, &SlotMapper::map);
connect(KelvinDblSpinbox, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), &mySlotMapper, &SlotMapper::map);
connect(RankineDblSpinbox, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), &mySlotMapper, &SlotMapper::map);
Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • Just a comment here I don't have access to Qt right now, and there are a ton of moving parts in this, so I imagine that I've got some syntax errors in here. If you could help me be pointing any out, I'd be most grateful. – Jonathan Mee Mar 08 '17 at 15:26
  • Thank you so much for posting this, I will look into this when I have some time, I will let you know what I come up with (I'm a novice at C++/Qt so this will take me a bit to process and understand). – spartan228 Mar 08 '17 at 17:12
  • @spartan228 Let me know if you have specific questions. I'm basically just calling the conversion functions and passing the values to all the other `QDoubleSpinBoxe`s when `map` is signaled. – Jonathan Mee Mar 08 '17 at 17:46
1

QSignalMapper is obsolete in C++11. It's only a band-aid against the cumbersomeness of declaring functors in C++98. If you're using a C++11 compiler, you never have to use QSignalMapper.

Lambdas make it trivial - and you really don't need to pass much. Side note: ideally, you should use a modern C++11 units library.

Here's a complete example:

// https://github.com/KubaO/stackoverflown/tree/master/questions/tempconvert-42648860
#include <QtWidgets>

const char kUnit[] = "unit";
class MyWidget : public QWidget {
   QFormLayout m_layout{this};
   QDoubleSpinBox m_fahrenheit, m_celsius, m_kelvin;
   QList<QDoubleSpinBox*> const m_spinBoxes{&m_fahrenheit, &m_celsius, &m_kelvin};
   enum Unit { Fahrenheit, Celsius, Kelvin };

   static double Q_DECL_RELAXED_CONSTEXPR fromK(Unit to, double val) {
      if (to == Fahrenheit) return (val-273.15)*1.8 + 32.0;
      else if (to == Celsius) return val - 273.15;
      else return val;
   }
   static double Q_DECL_RELAXED_CONSTEXPR toK(Unit from, double val) {
      if (from == Fahrenheit) return (val-32.0)/1.8 + 273.15;
      else if (from == Celsius) return val + 273.15;
      else return val;
   }
   void setTemp(Unit unit, double temp,  QDoubleSpinBox * skip = nullptr) {
      for (auto spin : m_spinBoxes) if (spin != skip) {
         QSignalBlocker b{spin};
         spin->setValue(fromK(unitOf(spin), toK(unit, temp)));
      }
   }
   static Unit unitOf(QDoubleSpinBox * spin) {
      return static_cast<Unit>(spin->property(kUnit).toInt());
   }
public:
   MyWidget(QWidget * parent = nullptr) : QWidget{parent} {
      m_layout.addRow("Fahreneheit", &m_fahrenheit);
      m_layout.addRow("Celsius", &m_celsius);
      m_layout.addRow("Kelvin", &m_kelvin);
      m_fahrenheit.setProperty(kUnit, Fahrenheit);
      m_celsius.setProperty(kUnit, Celsius);
      m_kelvin.setProperty(kUnit, Kelvin);
      for (auto const spin : m_spinBoxes) {
         auto const unit = unitOf(spin);
         spin->setRange(fromK(unit, 0.), fromK(unit, 1000.));
         connect(spin, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
                 [=]{ setTemp(unit, spin->value(), spin); });
      }
      setTemp(Celsius, 20.);
   }
};

int main(int argc, char ** argv) {
   QApplication app{argc, argv};
   MyWidget ui;
   ui.show();
   return app.exec();
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • This resets the value on the box changed, which I found particularly problematic, because round tripping isn't guaranteed. For example at unreasonably large Celsius values this can cause an infinite loop 0.0 – Jonathan Mee Mar 09 '17 at 13:22
  • I mean I was hoping you'd show me a workaround I hadn't figured out. (I didn't know about `Q_DECL_RELAXED_CONSTEXPR`, so that's already something.) Anyway this still trips the infinite loop, cause `setValue` sends the `valueChanged` signal, meaning that even though, the Celsius box won't keep telling itself to update, the Kelvin/Fahrenheit boxes will, and the round tripping can still cause an infinite loop. I had to get around this by blocking the signals :( – Jonathan Mee Mar 09 '17 at 13:52
  • The loops were not possible due to signal blocking. And the roundtrip errors are way past any realizable temperature measurement. 53 bits of mantissa is a **lot** of resolution. If we're ever capable of measuring temperature that well, I'll print that code out and eat it. – Kuba hasn't forgotten Monica Mar 09 '17 at 13:52
  • "setValue sends the valueChanged signal" No, it doesn't. That's what you missed. It never did send the signal - not in this code. The signal blocking was there from the first version of the code. "I had to get around this by blocking the signals" That's what my code did, too. And that's what you're supposed to do. It's not a hack. That's why signal blocking exists. – Kuba hasn't forgotten Monica Mar 09 '17 at 13:53
  • What am I missing? The Qt documentation says that `SetValue` sends the `ValueChanged` signal: http://doc.qt.io/qt-5/qdoublespinbox.html#value-prop – Jonathan Mee Mar 09 '17 at 13:54
  • 1
    You're missing the `QSignalBlocker`. Anyway, this is code that compiles and runs and does what it's supposed to do correctly. Just download it from github, run it under the debugger, and see for yourself how it works. Set breakpoints and experiment. – Kuba hasn't forgotten Monica Mar 09 '17 at 13:55
  • Aaand I can't read. Missed the `QSignalBlocker`. Thanks Have a +1! – Jonathan Mee Mar 09 '17 at 13:56