© W. David Ashley and Andrew Krause 2019
W. David Ashley and Andrew KrauseFoundations of PyGTK Developmenthttps://doi.org/10.1007/978-1-4842-4179-0_12

12. Custom Widgets

W. David Ashley1  and Andrew Krause2
(1)
AUSTIN, TX, USA
(2)
Leesburg, VA, USA
 

By now, you have learned a great deal about GTK+ and its supporting libraries. You have enough knowledge to use the widgets provided by PyGTK to create complex applications of your own.

However, one thing that you have not yet learned is how to create your own widgets. Therefore, this chapter is dedicated to deriving new classes from existing GTK+ classes. You are guided through some examples to show you how easy this is done using PyGTK.

In this chapter, you learn how to derive new classes and widgets from GTK+ widgets. We provide several examples of how to do this and discuss some of the problems you might encounter along the way.

An Image/Label Button

Since GTK+ 3.1, all stock items have been deprecated. While I agree with this decision, I was disappointed that the Gtk.Button was not extended to include an option for a button to display both an image and text. After eliminating the use-stock property, a Gtk.Button can only display text or an image, but not both at the same time.

The workaround for this is easily implemented but is extremely repetitive, and it is not object-oriented at all. You can see an example of how the workaround is implemented in the “Using Push Buttons” section. You can easily see that this solution would be very repetitive if you have a lot of buttons to code, and you are not making good use of code reuse with this implementation.

Another point of contention is that the programmer is forced to look up the real image they want from a string. What if the new implementation did that work for you and all you needed to supply to the new widget was the lookup string? After all, you probably want to use an image from the user’s default theme, so just let the new widget do all that work.

Figure 12-1 shows an image label button created by the program shown in Listing 12-1. This simple implementation shows how to extend the functionality and style of a standard Gtk.Button .
../images/142357_2_En_12_Chapter/142357_2_En_12_Fig1_HTML.jpg
Figure 12-1

An ImageLabelButton at work

Listing 12-1 shows the class implementation for the ImageLabelButton.
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class ImageLabelButton(Gtk.Button):
    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL,
                  image="image-missing", label="Missing", *args,
                  **kwargs):
        super().__init__(*args, **kwargs)
        # now set up more properties
        hbox = Gtk.Box(orientation, spacing=0)
        if not isinstance(image, str):
            raise TypeError("Expected str, got %s instead." % str(image))
        icon_theme = Gtk.IconTheme.get_default()
        icon = icon_theme.load_icon(image, -1,
                                    Gtk.IconLookupFlags.FORCE_SIZE)
        img = Gtk.Image.new_from_pixbuf(icon)
        hbox.pack_start(img, True, True, 0)
        img.set_halign(Gtk.Align.END)
        if not isinstance(label, str):
            raise TypeError("Expected str, got %s instead." % str(label))
        if len(label) > 15:
            raise ValueError("The length of str may not exceed 15 characters.")
        labelwidget = Gtk.Label(label)
        hbox.pack_start(labelwidget, True, True, 0)
        labelwidget.set_halign(Gtk.Align.START)
        self.add(hbox)
class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(25)
        button = ImageLabelButton(image="window-close", label="Close")
        button.connect("clicked", self.on_button_clicked)
        button.set_relief(Gtk.ReliefStyle.NORMAL)
        self.add(button)
        self.set_size_request(170, 50)
    def on_button_clicked(self, button):
        self.destroy()
class Application(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self,
                                    title="ImageLabelButton")
            self.window.show_all()
            self.window.present()
if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)
Listing 12-1

ImageLabelButton Class Implementation

The first point to understand is that when a Gtk.Button is created, the style of the button is set when you assign either the image or label property. Once assigned, the style of the button can never be changed. That is also the case for the new ImageLabelButton.

To start our discussion, let’s take a closer look at the initialization of the widget. We allow two new properties and override one Gtk.Button existing property. The property label overrides the parent property but is used in the same way as the text for the label widget. The properties orientation and image are new. They are used, respectively, to specify the orientation of the label/image (horizontal or vertical) and the string name to look up the corresponding default theme icon.

The rest of the initialization code is straightforward. Create a Gtk.Box with either the default orientation or the one specified by the keyword argument. Next, if the image keyword is specified, look up the name in the default user theme, fetch the icon, and add the image to Gtk.Box. Next, if the label is specified, create a Gtk.Label and add that to Gtk.Box. Lastly, add the box to the button.

We changed the Gtk.ImageLabelButton class by adjusting the alignment of the image and the label text so that they remain centered together no matter how the button is sized. We used the set_halign() method and turned off the fill and expand properties used in the pack_start() method.

Note that we do not override any other methods or properties of the underlying Gtk.Button . In this case, there is no need to modify the button in any other way. ImageLabelButton behaves as a normal Gtk.Button would. Therefore, we have accomplished our mission of creating a new class of button.

Most importantly, there is some error detection code in the new class to catch invalid data types and values. It cannot be stressed enough that you provide this kind argument checking. The lack of proper error messages and proper error detection can ruin all the work you put into a new class because it does not provide enough debug information to correct even minor mistakes or problems, which will cause your class to fall into disuse.

Custom Message Dialogs

Another reason to subclass GTK+ widgets is to save work by integrating more behavior into the widget. For instance, a standard GTK+ dialog requires a lot of initialization before you ever display the dialog. You can solve a repeated amount of work by integrating a standard look-and-feel to all of your message dialogs.

The way to reduce the amount of work necessary to create a dialog is to create a design that includes all the features you need, with either default settings or parameters that can activate additional options/values. In Listing 12-2, let’s look at a customized question dialog to see how this can work.
class ooQuestionDialog(Gtk.Dialog):
hbox = None
vbox = None
    def __init__(self, title="Error!", parent=None,
                  flags=Gtk.DialogFlags.MODAL, buttons=("NO",
                  Gtk.ResponseType.NO, "_YES",
                           Gtk.ResponseType.YES)):
        super().__init__(title=title, parent=parent, flags=flags,
                         buttons=buttons)
        self.vbox = self.get_content_area()
        self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        icon_theme = Gtk.IconTheme.get_default()
        icon = icon_theme.load_icon("dialog-question", 48,
                                    Gtk.IconLookupFlags.FORCE_SVG)
        image = Gtk.Image.new_from_pixbuf(icon)
        self.hbox.pack_start(image, False, False, 5)
        self.vbox.add(self.hbox)
    def set_message(self, message, add_msg=None):
        self.hbox.pack_start(Gtk.Label(message), False, False, 5)
        if add_msg != None:
           expander = Gtk.Expander.new_with_mnemonic( "_Click
                me for more information.")
           expander.add(Gtk.Label(add_msg))
           self.vbox.pack_start(expander, False, False, 10)
    def run(self):
        self.show_all()
        response = super().run()
        self.destroy()
        return response
Listing 12-2

A Customized Question Dialog Implementation

This dialog has a predefined design that is common to all of our message dialogs. It contains the following elements.
  • There are separate classes for each type of message dialog.

  • The dialog always contains an icon. The icon displayed is dependent on the type of dialog being displayed (message, information, error, etc.).

  • The dialog always displays a primary message.

  • The number and type of buttons displayed have a logical default that can be overridden by the user.

  • All dialogs default to modal.

  • An additional message can also be displayed in the dialog. It is enclosed in an expander that can be used any time the dialog is displayed.

  • There are two additional methods supplied with the class. The first method, set_message(), sets both the primary dialog message and an optional additional message. The second method, run(), shows the dialog, runs the dialog, destroys the dialog, and returns the response_id. The run() method is optional if you want a non-modal dialog displayed. Of course, you have to supply additional functionality in the run() dialog to make that happen.

It is very simple to instantiate and run the dialog. The following code performs all the necessary tasks to open the dialog.
dialog = ooQuestionDialog(parent=parentwin)
dialog.set_message("This is a test message. Another line.",
                   add_msg="An extra message line.”)
response = dialog.run()

It is obvious that loading the custom design into the dialog has both advantages and disadvantages. The main disadvantage is combining the design and the functionality together. The big advantage is that should you wish to change the design, there is only one place to modify it.

From this example, it should be an easy exercise for the user to create similar subclasses for error, message, information, and warning dialogs. Just remember that consistency is the key to this task.

Multithreaded Applications

Multithreaded applications are at the core of any high-end GTK+ application, which is any application that utilizes databases, network communication, client-server activities, interprocess communications, and any other process that uses long running transactions. All of these applications require either multiple processes or threads to manage the communications to and from the separate entities to supply and receive information from each other.

GTK+ is a single thread library. It is not thread safe to access its API from multiple threads. All API calls must come from the main thread of the application. This means that long-running transactions can make the user interface seem to freeze, sometimes for very long periods of time.

The key to solving this problem is to move all long-running transactions to other threads. But, this is not easy because it involves setting up threads and supplying some type of thread safe communications for two or more threads or processes to utilize.

Most books on the topic of GUIs usually ignore this problem and concentrate on the GUI itself. This is a great disservice to the reader because just about any GUI application that the reader encounters in their professional life is multithreaded, but the reader has no experience in this type of application.

This book supplies an example to give you a better idea of what a multithreaded application looks like and the basics on how to organize it. The example is not the only way to architect a multithreaded application, but it does supply all the basics for such an application. The details and methods might be different for your project, but you are following the same basic outline supplied by our example.

Listing 12-3 is the example multithreaded application. It is a very simple program that requests information from another thread, and the main thread correctly waits for the supplier thread to provide the data. We describe this example in some detail after the listing.
#!/usr/bin/python3
import sys, threading, queue, time
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def dbsim(q1, q2):
    while True:
        data = q1.get()
        # the request is always the same for our purpose
        items = {'lname':"Bunny", 'fname':"Bugs",
                 'street':"Termite Terrace", 'city':"Hollywood",
                 'state':"California", 'zip':"99999", 'employer':"Warner
                 Bros.", 'position':"Cartoon character", 'credits':"Rabbit
                 Hood, Haredevil Hare, What's Up Doc?"}
        q2.put(items)
        q1.task_done()
class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lname    = None
        self.fname    = None
        self.street   = None
        self.city     = None
        self.state    = None
        self.zip      = None
        self.employer = None
        self.position = None
        self.credits  = None
        self.q1 = queue.Queue()
        self.q2 = queue.Queue()
        self.thrd = threading.Thread(target=dbsim, daemon=True,
                                     args=(self.q1, self.q1, self.q2))
        self.thrd.start()
        # window setup
        self.set_border_width(10)
        grid = Gtk.Grid.new()
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)
        # name
        label = Gtk.Label.new("Last name:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 0, 1, 1)
        self.lname = Gtk.Entry.new()
        grid.attach(self.lname, 1, 0, 1, 1)
        label = Gtk.Label.new("First name:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 0, 1, 1)
        self.fname = Gtk.Entry.new()
        grid.attach(self.fname, 3, 0, 1, 1)
        # address
        label = Gtk.Label.new("Street:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 1, 1, 1)
        self.street = Gtk.Entry.new()
        grid.attach(self.street, 1, 1, 1, 1)
        label = Gtk.Label.new("City:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 1, 1, 1)
        self.city = Gtk.Entry.new()
        grid.attach(self.city, 3, 1, 1, 1)
        label = Gtk.Label.new("State:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 2, 1, 1)
        self.state = Gtk.Entry.new()
        grid.attach(self.state, 1, 2, 1, 1)
        label = Gtk.Label.new("Zip:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 2, 1, 1)
        self.zip = Gtk.Entry.new()
        grid.attach(self.zip, 3, 2, 1, 1)
        # employment status
        label = Gtk.Label.new("Employer:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 3, 1, 1)
        self.employer = Gtk.Entry.new()
        grid.attach(self.employer, 1, 3, 1, 1)
        label = Gtk.Label.new("Position:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 3, 1, 1)
        self.position = Gtk.Entry.new()
        grid.attach(self.position, 3, 3, 1, 1)
        label = Gtk.Label.new("Credits:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 4, 1, 1)
        self.credits = Gtk.Entry.new()
        grid.attach(self.credits, 1, 4, 3, 1)
        # buttons
                      bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
        load_button = Gtk.Button.new_with_label("Load")
        bb.pack_end(load_button, False, False, 0)
        load_button.connect("clicked", self.on_load_button_clicked)
        save_button = Gtk.Button.new_with_label("Save")
        bb.pack_end(save_button, False, False, 0)
        save_button.connect("clicked", self.on_save_button_clicked)
        cancel_button = Gtk.Button.new_with_label("Cancel")
        bb.pack_end(cancel_button, False, False, 0)
        cancel_button.connect("clicked", self.on_cancel_button_clicked)
        # box setup
        vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL,
        spacing=5) vbox.add(grid)
        vbox.add(bb)
        self.add(vbox)
    def on_cancel_button_clicked(self, button):
        self.destroy()
    def on_load_button_clicked(self, button):
        self.q1.put('request')
        # wait for the results to be
        queued data = None
        while Gtk.events_pending() or data ==
            None: Gtk.main_iteration()
            try:
                data = self.q2.get(block=False)
            except queue.Empty:
                continue
        self.lname.set_text(data['lname'])
        self.fname.set_text(data['fname'])
        self.street.set_text(data['street'])
        self.city.set_text(data['city'])
        self.state.set_text(data['state'])
        self.zip.set_text(data['zip'])
        self.employer.set_text(data['employer'])
        self.position.set_text(data['position'])
        self.credits.set_text(data['credits'])
        self.q2.task_done()
    def on_save_button_clicked(self, button):
        self.lname.set_text("")
        self.fname.set_text("")
        self.street.set_text("")
        self.city.set_text("")
        self.state.set_text("")
        self.zip.set_text("")
        self.employer.set_text("")
        self.position.set_text("")
        self.credits.set_text("")
class Application(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Multi-Thread")
        self.window.show_all()
        self.window.present()
if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)
Listing 12-3

Multithreaded Application

Before we examine the listing in detail, let’s describe the application requirements and see how we satisfied those requirements.

Our application is a simulation of a database client and a server—all in a single multithreaded program. The main window requests data from the threaded server and waits for a response. The server waits for a request and then supplies the data back to the client. The client side of the application is a simple GTK+ application that displays the data fetched from the server. The server is a single Python function running in a thread. It waits for a request, provides the data, and then waits for the next request.

The key to all of this is that the GTK+ client does not freeze, no matter how long the server takes to provide the data back to the client. This allows the application (and all other applications) to continue processing desktop events.

Let’s start our examination of the listing right at the top—the dbsim server function, which stands for database simulator. We kept this function as simple as possible to reveal the basic functionality. The code is an endless loop that waits for a transaction to appear on a queue. q1.get() tries to read a transaction off the queue and waits to return when a transaction becomes available. dbsim does nothing with the transaction data; instead, it just builds a Python dictionary. It then puts the dictionary on a return queue with the q2.put(items). Finally, processing returns to the top of the forever loop and waits for the next transaction.

The solution shown here works fine for a single client, but breaks down when multiple clients try to access the server because there is no way to synchronize the client requests with the returned data. We would need to enhance the application to provide that level of synchronization.

If you want to experiment with longer transaction times from the server, insert a time.sleep() statement between the q1.get() and the q2.put(items) statements. This provides the proof that the client does not freeze during a long-running transaction.

Now let’s see how the client works. The client is a standard GTK+ application, except for the on_load_button_clicked() method. This method accesses the database simulator thread to obtain the information to fill out the entry fields displayed on the main window. The first task is to send the request to the database simulator. It does this by placing a request on a queue that is read by the simulator.

Now we come to the hard part. How do we wait for the returned information without putting the main thread to sleep? We do this by placing the method in a loop that processes pending events until the information is available from the server. Let’s take a look at that tight loop.
while Gtk.events_pending() or data == None:
    Gtk.main_iteration()
    try:
        data = self.q2.get(block=False)
    except queue.Empty:
        continue

The while statement starts the loop by checking to see if there are pending GTK+ events to process and whether data has been placed in the target variable. If either condition is True, the tight loop is entered. Next, we process a single GTK+ event (if one is ready). Next, we try to fetch data from the server. self.q2.get(block=False) is a non-blocking request. If the queue is empty, then an exception is raised and then ignored because we need to continue the loop until the data is available.

Once the data is successfully fetched, the on_load_button_clicked() method continues by filling out the displayed entry fields with the supplied information.

There is one more piece to this puzzle. Take a look at the statement that created the server thread.
self.thrd = threading.Thread(target=dbsim, daemon=True, args=(self.q1, self.q2))

The key part of this statement is the daemon=True argument, which allows the thread to watch for the main thread to finish, and when it does, it kills the server thread so that the application ends gracefully.

This application example has all the basic for communication between two threads. We have two queues for requests and returned data. We have a thread that performs all the long-running transactions needed by the client. And finally, we have a client that does not freeze while waiting for information from the server. This is the basic architecture for a multithreaded GUI application.

The Proper Way to Align Widgets

Prior to GTK+ 3.0, the proper way to align widgets was through the Gtk.Alignment class. This class was deprecated starting with GTK+ 3.0, thus seeming to eliminate an easy way to align widgets. But in truth, there are two methods in the Gtk.Widget class that can align widgets in any container: the halign() and the valign() methods.

These methods are easy to use and provide the type of alignment that the programmer desires in 90% of cases. Listing 12-4 shows how using the Gtk.Widget alignment methods produce all the types of alignment provided by the halign() and valign() methods.
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs) :
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.resize(300, 100)
        # create a grid
        grid1 = Gtk.Grid()
        grid1.height = 2
        grid1.width = 2
        grid1.set_column_homogeneous(True)
        grid1.set_row_homogeneous(True)
        self.add(grid1)
        # build the aligned labels
        label1 = Gtk.Label('Top left Aligned')
        label1.can_focus = False
        label1.set_halign(Gtk.Align.START)
        label1.set_valign(Gtk.Align.START)
        grid1.attach(label1, 0, 0, 1, 1)
        label2 = Gtk.Label('Top right Aligned')
        label2.can_focus = False
        label2.set_halign(Gtk.Align.END)
        label2.set_valign(Gtk.Align.START)
        grid1.attach(label2, 1, 0, 1, 1)
        label3 = Gtk.Label('Bottom left Aligned')
        label3.can_focus = False
        label3.set_halign(Gtk.Align.START)
        label3.set_valign(Gtk.Align.END)
        grid1.attach(label3, 0, 1, 1, 1)
        label4 = Gtk.Label('Bottom right Aligned')
        label4.can_focus = False
        label4.set_halign(Gtk.Align.END)
        label4.set_valign(Gtk.Align.END)
        grid1.attach(label4, 1, 1, 1, 1)
class Application(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
        gtk_version = float(str(Gtk.MAJOR_VERSION)+'.'+str(Gtk.MINOR_VERSION))
        if gtk_version < 3.16:
           print('There is a bug in versions of GTK older that 3.16.')
           print('Your version is not new enough to prevent this bug from')
           print('causing problems in the display of this solution.')
           exit(0)
    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self,
                                    title="Alignment")
            self.window.show_all()
            self.window.present()
if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)
Listing 12-4

Aligning Widgets

When you run this example, you see four different alignments displayed, as shown in Figure 12-2.
../images/142357_2_En_12_Chapter/142357_2_En_12_Fig2_HTML.jpg
Figure 12-2

Alignment example

The following code snippet shows how to align a single label widget to the top-left corner of a Gtk.Grid cell.
label1.set_halign(Gtk.Align.START)
label1.set_valign(Gtk.Align.START)

As you can see, aligning a widget is really simple, and the overhead is reduced because we are not invoking a new class for each aligned widget. This method of aligning widgets should be sufficient for most of your application needs.

Summary

This chapter presented three widget customization examples, which should provide enough information for you to create your own custom widgets. There are many more possibilities to increase the usability and quality of your applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.67.5