This is a wrapper around the Python Tkinter and GTK+ 3 bindings. You can write code using BananaGUI, and then run the same code using any of these toolkits. BananaGUI may feature PyQt5 support later. BananaGUI requires Python 3.3 or newer.
A minimal Hello World program in BananaGUI looks like this:
from bananagui import load_wrapper, mainloop, widgets
load_wrapper('tkinter')
window = widgets.Window("Hello World")
window.add(widgets.Label("Hello World!"))
window.on_close.connect(mainloop.quit)
mainloop.run()You can also write your GUI using the .ini format and then load it with
bananagui.iniloader:
from bananagui import iniloader, load_wrapper, mainloop
# Usually this would be in another file.
INI = """\
from bananagui import widgets
[window]
class = widgets.Window
title = "Hello World 3"
[label in window]
class = widgets.Label
text = "Hello World!"
"""
def main():
load_wrapper('tkinter')
widgetdict = iniloader.loads(INI)
widgetdict['window'].on_close.connect(mainloop.quit)
mainloop.run()
if __name__ == '__main__':
main()See the guitests directory for more examples or read the tutorial.
Many Python beginners have had a conversation like this:
<beginner> I want to make a simple GUI program with Python.
Which GUI toolkit should I use?
<qtfanboy> Use PyQt, it's cross-platform.
<windowsuser> I hate PyQt. It's a huge library with more than
enough features, installing the Windows version is
a real pain and their licensing solution sucks. Use
tkinter, it comes with Python so you don't need to
install anything.
<linuxuser> Tkinter looks like crap on my GTK+ based desktop,
and it doesn't come preinstalled on my system. Use
GTK+, it comes with my system and looks great.
<windowsuser> But installing GTK+ on Windows is even worse than
installing PyQt! It also looks awful on Windows.
As you can see, choosing the right GUI toolkit is not easy. Each toolkit has its pros and cons, and none of them satisfies everyone's needs. In the worst possible case, people choose one toolkit and later rewrite the program using another toolkit. I have done this several times.
This is when BananaGUI comes in. It allows you to write your application once, and then run it with tkinter or GTK+ 3. Future versions of BananaGUI will probably provide Qt support also. BananaGUI isn't meant to replace any of these existing toolkits, it just provides a high-level way to use them.
PyQt and GTK+ Python bindings don't provide good documentation
strings for help(). For example, let's say that we want to know
something about checkboxes. This example uses GTK+, but PyQt works
similarly.
>>> from gi.repository import Gtk
>>> help(Gtk.CheckButton)
Help on class CheckButton in module gi.repository.Gtk:
class CheckButton(ToggleButton)
| :Constructors:
|
| ::
|
| CheckButton(**properties)
| new()
| new_with_label(label:str)
| new_with_mnemonic(label:str)
|
| Method resolution order:
| CheckButton
| ToggleButton
| gi.overrides.Gtk.Button
| Button
| Bin
| gi.overrides.Gtk.Container
| Container
| gi.overrides.Gtk.Widget
| Widget
| gi.repository.GObject.InitiallyUnowned
| gi.overrides.GObject.Object
| gi.repository.GObject.Object
| gi._gobject._gobject.GObject
| gi.repository.Atk.ImplementorIface
| Buildable
| Actionable
| Activatable
| gobject.GInterface
| builtins.object
|
| Data descriptors defined here:
|
| toggle_button
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __gsignals__ = {}
|
| __gtype__ = <GType GtkCheckButton (30187408)>
|
| __info__ = ObjectInfo(CheckButton)
|
| do_draw_indicator = gi.VFuncInfo(draw_indicator)
| draw_indicator(self, cr:cairo.Context)
|
| new = gi.FunctionInfo(new)
| new()
|
| new_with_label = gi.FunctionInfo(new_with_label)
| new_with_label(label:str)
|
| new_with_mnemonic = gi.FunctionInfo(new_with_mnemonic)
| new_with_mnemonic(label:str)
|
...
We have the names of all methods and information about the arguments they take, but that's about it. There are no descriptions about what the methods actually do. Instead, there are many implementation details that we are not interested in and a ton of methods that we don't use 99% of the time. Tkinter is a little bit better when it comes to this, but it's not perfect either.
I try my best to add good docstrings everywhere in BananaGUI to make
help() as informative as possible. Here's the same thing in
BananaGUI:
>>> from bananagui import widgets
>>> help(widgets.Checkbox)
Help on class Checkbox in bananagui.widgets:
bananagui.widgets.Checkbox = class Checkbox(Child)
| A widget that can be checked.
|
| ,-------------------.
| | | Check me! |
| `-------------------'
|
| ,-------------------.
| | X | Uncheck me! |
| `-------------------'
|
| The Checkbox widget has nothing to do with the Box widget.
|
| Method resolution order:
| Checkbox
| Child
| Widget
| builtins.object
|
| Methods defined here:
|
| __init__(self, text='', *, checked=False, **kwargs)
| Initialize the checkbox and set arguments as attributes.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| checked
| True if the checkbox is currently checked.
|
| on_checked_changed
| This callback is ran when checked changes.
...
I think this is a lot better. We can get a good idea of what the
Checkbox is and how it works with just help(). There are less useless
implementation details showing up, and we even have ascii art pictures
of the widget.
BananaGUI doesn't implement the methods that we don't usually need, but
it exposes the real GUI toolkit widget it uses internally and it can be
accessed like some_bananagui_widget.real_widget.
When writing BananaGUI, I try to create informative error messages and
representations to make debugging easier. For example, let's create a
checkbox with tkinter and then have a look at it on the interactive
>>> prompt. This problem isn't tkinter-specific, GTK+ and PyQt also
have this.
>>> import tkinter as tk
>>> root = tk.Tk()
>>> checkbox = tk.Checkbutton(root, text="Check me!")
>>> checkbox
<tkinter.Checkbutton object at 0x7f38ac29dd30>
>>>So we know that it's a tkinter.Checkbutton and we know its memory
address. But if we have more than one checkbox, that isn't really useful
for distinguishing them from each other.
Let's do the same thing with BananaGUI:
>>> from bananagui import load, widgets
>>> load('tkinter')
>>> checkbox = widgets.Checkbox("Check me!")
>>> checkbox
<bananagui.widgets.Checkbox object, text='Check me!', checked=False>I think this is a lot better. The __repr__ method returned a good
description of the widget. It's brief, but a lot more helpful than a
memory address.
BananaGUI consists of the public API and the bananagui.wrappers
submodule. The wrappers submodule contains a bunch of modules that each
"wrap" a GUI toolkit and provide a way to use it. The
bananagui.load_wrapper() function simply imports one of these wrapper
modules and sets its name to bananagui._wrapper, and rest of BananaGUI
finds it there. Most of the public API uses bananagui._get_wrapper()
to access the currently loaded wrapper.
BananaGUI uses three kinds of tests:
- Unit tests in the tests directory.
- Doctests in files.
- GUI tests are small programs written using BananaGUI.
The BananaGUI wrappers are meant to be tested entirely with GUI tests because it's an easy way to make sure that everything works.
$ yourpython -m guitests some_wrapper
Of course, replace yourpython with a working Python program and
some_wrapper with a valid argument to bananagui.load_wrapper(). See
help('bananagui.load_wrapper').
You need to install pytest to run the unit tests and doctests:
$ yourpython -m pytest
If you're interested in the coverage you can install coverage.py and run pytest with it:
$ yourpython -m coverage run -m pytest
$ yourpython -m coverage report --include='bananagui/*'
Or you can generate a nice HTML report:
$ yourpython -m coverage html --include='bananagui/*'
$ yourpython -m webbrowser htmlcov/index.html
Keep in mind that the coverage of bananagui.widgets looks worse than
it really is because a lot of the testing is taken care of by the
guitests.
I want to thank these people for helping me write BananaGUI: