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 is 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.
September 25, 2010 at 8:29 am |
Thanks a lot for your ruby articles…
Maybe you could get more expose for your articles by posting them in rubyflow or rubyinside sites. (I’m not affiliated, although I subscribed them via RSS like planet KDE)
I will subscribe your blog too… in case you decide not to publish articles on those sites
Best regards and keep posting
September 25, 2010 at 12:49 pm |
@Paulo
Just posted on rubyflow, thanks for the suggestion.
September 25, 2010 at 4:15 pm |
And that’s why a found your nice post. Good work. Guess i have to try it out.
September 27, 2010 at 7:15 am |
Ciao,
scrivo qui perché non ho trovato una mail di riferimento sul tuo blog, vorrei invitarti a collaborare con noi di qt-italia.org, non abbiamo nessuno bravo ed esperto come te e ci farebbe piacere la tua presenza.
Daniele
November 16, 2010 at 8:55 pm |
Wow, I worked on Qt using Python some years ago, I will try now with Ruby.
Thanks for your posts.
December 9, 2010 at 5:59 pm |
So is your ‘on’ method a method of Qt::Base?
Can you give the whole signature [requires, includes, etc] because my build of Qt4 doesn’t seem to have ‘Signal’ .. ..
December 9, 2010 at 11:20 pm |
Ahh, I see, it is part of your kaya work, which looks really nice. It would be great to see your utils/wrappers brought out into their own library so that others [like me] can easily use them in their own projects.
December 10, 2010 at 6:10 pm
Yes, I’ve been working for a while on extracting its GUI abstraction into a library. It’s not exactly straightforward, some parts were ad-hoc for kaya, and I didn’t think at the time that they could be useful independently, but it’s coming along.
Hopefully, it should land on github some time soon.
December 14, 2010 at 11:50 pm |
One comment about the ‘never use slots’ guideline that you have here.
My guess is that if you’re not actually doing any processing on the data from a signal, just passing the args along to another QObject [which would have a slot for this signal], that using ruby to connect a signal to a slot would be significantly faster than using the ruby widget.on block style. I assume this would simply connect the sig/slot and not invoke any ruby code unless the slot is in fact defined on the ruby side.
Obviously the ruby style is more elegant, but I figure it might be useful to keep this in mind when doing profiling.
Am I totally off base here?
March 16, 2013 at 8:15 pm |
Hi, you wrote that using the signal/slot concept is totally useless.
If I use the uic to compile the user interface file i got a rb file which contains some code but also with connect …. as I know it from c++.
So I tried out the same way but i fails because of the following error:
“…No superclass method connect…”. So I started searching in google and so I found your page.
What I wanna do is in a derived class (Baseclass is Qt::MainWindow) do a connect from a variable to a progressbar, but at the moment I’m totally lost how this should work. Do you have any idea?
example:
class Test
def start
@cnt = @cnt + 1
end
end
if @cnt changes then a calculation should be started and the result should be displayed in the Qt progressbar.
rgds
Michael