Effective Qt in ruby (part 2)
In the first part of this series, I listed some of the reasons why you should consider writing your Qt/KDE applications in ruby. This post details some of the technical differences between writing Qt code in C++ and in ruby.
One of the first problems that pop up when starting a new Qt/KDE project in ruby is how to use it in such a way that your code doesn’t end up being completely unidiomatic. This can happen very easily if one tries to stick to the usual conventions that apply when writing Qt code in C++.
If you take any piece of C++ code using Qt, you can very trivially translate it into ruby. That works, and sometimes it’s useful, but writing code in this way completely misses the point of using a dynamic language. You might as well write directly in C++, and enjoy the improved performance.
So I believe it’s important to identify the baggage that Qt brings from its C++ roots, and eliminate it when using it from ruby. Here are some ideas to achieve that.
Use the ruby convention for method names
A minor point, but important for code readability.
Qt uses camel case for method names, while ruby methods are conventionally written with underscores. Mixing the two styles inevitably results in an unreadable mess, so the ruby convention should be used at all times.
Fortunately, QtRuby allows you to call C++ methods by spelling their name with underscores, so it’s quite easy to achieve a satisfactory level of consistency with minimum effort.
Never declare signals
The signal/slot mechanism is a very important Qt feature, because it allows to work around the static nature of C++ by allowing dynamic calls to methods. You won’t need that in ruby. For instance, you can use the standard observer library to fire events and set callbacks. It’s completely dynamic and there’s no need to define your signals beforehand.
Never use slots
Slots are useless in ruby. QtRuby allows you to attach a block to a connect call, and that is what you should always be using. Never use the SLOT function with a C++ signature.
Avoid C++ signatures altogether
This seems impossible. It might be easy to use symbols (without using the SIGNAL
“macro”) to specify signals with no arguments, like
button.on(:clicked) { puts "hello world" }
but if a signal has arguments, and possibly overloads, specifying only its name doesn’t seem to be enough to determine which particular overload we are interested in.
Indeed, it’s not possible in general, but you can disambiguate using the block arity for most overloaded signals, and add type annotations in those rare cases where the arity is not enough.
Here is my on
method, which accomplishes this:
def on(sig, types = nil, &blk) sig = Signal.create(sig, types) candidates = if is_a? Qt::Object signal_map[sig.symbol] end if candidates if types # find candidate with the correct argument types candidates = candidates.find_all{|s| s[1] == types } end if candidates.size > 1 # find candidate with the correct arity arity = blk.arity if blk.arity == -1 # take first candidates = [candidates.first] else candidates = candidates.find_all{|s| s[1].size == arity } end end if candidates.size > 1 raise "Ambiguous overload for #{sig} with arity #{arity}" elsif candidates.empty? msg = if types "with types #{types.join(' ')}" else "with arity #{blk.arity}" end raise "No overload for #{sig} #{msg}" end sign = SIGNAL(candidates.first[0]) connect(sign, &blk) SignalDisconnecter.new(self, sign) else observer = observe(sig.symbol, &blk) ObserverDisconnecter.new(self, observer) end end
The Signal
class maintains the signal name and (optional) specified types. The method lazily creates a signal map for each class, which maps symbols to C++ signatures, and proceeds to disambiguate among all the possibilities by using types, or just the block arity, when no explicit types are provided. If no signal is found, or if the ambiguity could not be resolved, an exception is thrown.
For example, the following line:
combobox.on(:current_index_changed, ["int"]) {|i| self.index = i }
is referring to currentIndexChanged(int)
and not to the other possible signal currentIndexChanged(QString)
, because of the explicit type annotation.
The advantage of this trick is that I can write, for example:
model.on(:rows_about_to_be_inserted) do |p, i, j| # ... end
without specifying any C++ signature, which in this case would be quite hefty:
rowsAboutToBeInserted(const QModelIndex& parent, int start, int end)
Conclusion
QtRuby is an exceptional library, but to use it effectively you need to let go of some of the established practices of Qt programming in C++, and embrace the greater dynamicity of ruby.
In the next article I’ll show you how I tried to push this idea to the extreme with AutoGUI, a declarative GUI DSL built on top of QtRuby.
Comments