I'd suggest letting the plugin worry about the types of its arguments, and have a separate component which knows how to map each type onto a GUI control.
This is almost a straight model/view decomposition, so seems like a well-understood idiom.
Now, your type model can be enumerated, or you can use the arguably more OO Visitor pattern, but you're still essentially coming up with a fixed and not-really-extensible type system ahead of time. Is that adequate?
You'll probably end up with some type that knows both the specific derived type of a given argument, and the details of how to render it in Qt. This would handle the Qt signals, and pass values back to the argument.
... Through attempting a dynamic_cast or reading some kind of identification code such as an enum, I'm thinking. I still don't see how the Visitor DP could be used instead of these ...
The Visitor pattern is specifically used to avoid dynamic_cast
, so I'm not sure what the confusion is here. Admittedly there's a post-hoc version which does use dynamic_cast
, but that's hidden away in the implementation and isn't the usual case anyway.
So, for a concrete example, let's create a model with a couple of argument types:
struct ArgumentHandler; // visitor
class Argument { // base class for visitable concrete types
public:
virtual void visit(ArgumentHandler&) = 0;
};
// sample concrete types
class IntegerArgument: public Argument {
int value_;
public:
IntegerArgument(int value = 0) : value_(value) {}
void set(int v) { value_ = v; }
int get() const { return value_; }
virtual void visit(ArgumentHandler&);
};
class BoundedIntegerArgument: public IntegerArgument
{
int min_, max_;
public:
virtual void visit(ArgumentHandler&);
// etc...
};
Now we have some concrete types for it to visit, we can write the abstract visitor
struct ArgumentHandler {
virtual ~ArgumentHandler() {}
virtual void handleInteger(IntegerArgument&);
virtual void handleBoundedInteger(BoundedIntegerArgument&);
// ...
};
and our concrete types implement visitation like so:
void IntegerArgument::visit(ArgumentHandler& handler) {
hander.handleInteger(*this);
}
void BoundedIntegerArgument::visit(ArgumentHandler& handler) {
hander.handleBoundedInteger(*this);
}
Now, we can write an abstract plugin only in terms of the data model types - it doesn't need to know anything about the GUI toolkit. Let's say we just provide a way to query its arguments for now (note that each concrete subtype should have set/get methods)
class PluginBase
{
public:
virtual int arg_count() const = 0;
virtual Argument& arg(int n) = 0;
};
Finally, we can sketch a View that knows how to interrogate an abstract plugin for its arguments, how to display each concrete argument type, and how handle inputs:
// concrete renderer
class QtView: public ArgumentHandler
{
struct Control {};
struct IntegerSpinBox: public Control {
QSpinBox control_;
IntegerArgument &model_;
};
struct IntegerSlider: public Control {
QSlider control_;
BoundedIntegerArgument &model_;
};
std::vector<std::unique_ptr<Control>> controls_;
public:
// these overloads know how to render each argument type
virtual void handleInteger(IntegerArgument &arg) {
controls_.push_back(new IntegerSpinBox(arg));
}
virtual void handleBoundedInteger(BoundedIntegerArgument &arg) {
controls_.push_back(new IntegerSlider(arg));
}
// and this is how we invoke them:
explicit QtView(PluginBase &plugin) {
for (int i=0; i < plugin.arg_count(); ++i) {
plugin.arg(i).visit(*this);
}
}
};
I've omitted all the virtual destructors, the Qt signal handling, and lots more. But, hopefully you can see how a QtView::IntegerSpinBox
object could handle the valueChanged
signal from its captive spinbox widget, and call model_.set()
to push that back to the plugin.