📜 ⬆️ ⬇️

More about layout and widgets

This is a continuation of a series of articles on PyGTK.

In the previous article, we prepared Windows and Ubuntu to develop PyGTK applications, worked with the Glade interface editor, looked at the horizontal and vertical layout types, used the scroll area and text editor, the vertical group of buttons, the buttons, and, a little, the signals. As a result, we got the first real cross-platform application that successfully worked in Ubuntu and Windows. If you have not read the previous article , I recommend that you start with it.

In this article, we will create a simple game, and along the way we will learn a little more about the layout, continue acquaintance with PyGTK widgets, and work with the dialogs.

Idea script


First of all, I want to say what we will do. It will be a game. The game should be simple and fun. The simplest thing that occurred to me was tic-tac-toe. The game must have a character opponent. The coolest character I know is Bender. Therefore, we will make tic-tac-toe, with Bender as an opponent.
')
We take the algorithm for the game on Wikipedia : it is very simple and clearly described. I remind you that we are mastering PyGTK, so complex algorithms would increase from the main topic. If you want to know everything about algorithms, read Donald Knuth .

The scenario is as follows: the player will put crosses, Bender will answer with zeros. At the end of the game, a dialogue with the congratulations of the winner will be displayed, after which the game will start anew.

Immediately, I note that from the point of view of usability, the game is very bad, because the dialogs that overlap the main window will be used. The good news is that you get to know the dialogs, you can make modal dialogs from any windows.

Sketch the interface in Inkscape :


To create an interface file, we will use the Glade interface editor. Please run Glade. If you do not know what is being said, please read the previous article .

Tabular layout


The table layout is very convenient when you need to place a large number of widgets, this is our case.

From the outline you can see that you need three lines for daggers, as well as one line at a time to place the buttons above and below.
Create a new window

immediately make it visible (this will be the main window of our program),

and add a table layout

with five lines and three columns
.
I stretched the window in size so that the table cells were square, it's easier to present the final result.
.

Add two buttons. Place the button in the very first cell of the table:
.
As you can see, the button occupied the whole cell. We could use this feature to render the playing field, but we will not do that, and here we also do not need this behavior, so we switch to the “Packaging” tab in the properties. In the "Vertical Parameters" uncheck "Filling"
.
Thus, we indicate that the button should not fill all the available space in height.
.
If you noticed, the table cells are not the same width. This will prevent us from making a playing field with the same cells. Therefore, we choose table1, in the basic properties we change “Homogeneity” to “Yes”
,
the cells are the same in width.

On the outline, the upper and lower buttons occupy all the space horizontally, and now the upper button takes up only one cell. It needs to occupy three cells in width. Select the button, the properties "Packaging", "Adding to the right",

increase the value to three. The button is three cells wide.
.

Button with icons


It would be desirable that the New Game and Quit Game buttons contain not only text, but also icons. Make it pretty simple. In the basic properties, we change the “Edit Type” to “Container”
.
Now we can place anything in the container on the surface of the button.

The easiest and correct way to put there text with an icon is to use another layout tool - alignment. Add to the button "Alignment"
,
in the main properties we set the vertical and horizontal scaling to zero
,
thus, the alignment will not “capture” all the space available to it, but will be limited to the minimum necessary. We need this so that the icon does not crawl away from the text.

Inside the alignment there is room for only one element, and we need to place two. Use the horizontal layout with two cells, place it in alignment
.
Place the icon in the left cell
,
and in the right - a text label with the text "New game"
,
.

In order not to search and not to draw an icon, in order not to download it later at the start of the program, we will use ready-made GTK icons. Select the “Create” icon in the basic properties.
.

The icon is too close to the label. This can be corrected by setting the size of the gap for the horizontal layout, or by specifying the pixel addition for the icon itself. I set the addition to four pixels for the icon
.
The result looks like this:
.

Do the same for the bottom button “Exit Game”. It should turn out like this
,
I used the Close icon.

Save the interface to the gui.glade file, the playing field will be created when the game starts programmatically.

Playing field


We will build the playing field on the basis of text labels in order to better study their features.

There will be nine tags, and there is no point in working with each individually in the Glade, because you can do it in a few lines of code. In addition, then it will be possible to change some properties for all the tags at once, and we will need this for sure.

Create a procurement program:
 #! / usr / bin / env python
 # coding: utf-8
 import sys
 import os
 import pygtk
 pygtk.require ('2.0')
 import gtk
 import gtk.glade
        
 class App:

     def __init __ (self):
         # Load the interface file
         self.gladefile = "gui.glade"
         # interface tree
         self.widgetsTree = gtk.glade.XML (self.gladefile)
         # table        
         self.table1 = self.widgetsTree.get_widget ("table1")       
         # initialize the playing field   
         self.init_board ()
         # Connect the window closing event with the application termination function
         self.window = self.widgetsTree.get_widget ("window1")
         if (self.window):
             self.window.connect ("destroy", self.close_app)
           
     def close_app (self, widget):    
         gtk.main_quit ()        
    
 if __name__ == "__main__":
     app = App ()
     gtk.main ()

The playing field will be created in self.init_board (), we write it:
     def init_board (self):
         # dictionary storage labels of the playing field
         self.board_widgets = {}
         # we will add tags in lines 1-3
         for row in range (0,3):
             # initialize the storage line, create an empty dictionary
             # to store the contents of table cells
             self.board_widgets [row] = {}
             # we pass columns 0-2
             for column in range (0,3):
                 # create a label, its text includes the row and column number
                 self.board_widgets [row] [column] = \
                                 gtk.Label ("label_% d_% d"% (column, row))
                 # attach the label to the table
                 self.table1.attach (self.board_widgets [row] [column], 
                                    column, column + 1,
                                    row + 1, row + 2)
                 # make the label visible
                 self.board_widgets [row] [column] .show ()

The playing field is stored in a “two-dimensional array” created on the basis of dictionaries. The label constructor, gtk.Label (), is passed text that will display the label. The table attach method takes the first parameter to the widget, in this case our label. The second is the number of the column to which the left side of the label will be attached, the third is the number of the column to which the right side of the label will be attached. That is, in order to place a label in the very first column, the second and third parameters must be 0 and 1. The line in which the widget should be placed is indicated in the attach in exactly the same way, in the third and fourth parameter. Description of all parameters you can read in the documentation .

After running the program you should see something like this window:
.

Event Area, Frame


The moment has come to revive the program: let's make the lower button working, and also add a handler for clicking the left mouse button to each label. Open Glade to add a click handler to the button. Select the button, go to the "Signals" tab in the properties, and assign a handler for the clicked signal
.
Save the changes, go back to editing the code. Add to __init__ such lines:
         # Glossary defining the connection of events with handler functions
         dic = {                 
                 "button2_clicked_cb": self.close_app,
             }
         # Magic command connecting signals with handlers
         self.widgetsTree.signal_autoconnect (dic)

Save the code. You can check if the program closes when you press the bottom button.

Each label also needs a mouse click event handler, a button-press-event. But if you add it right now, run the program, and check if it works, then make sure it doesn't work. The label is not able to handle such events, because it does not have its own window. All widgets that do not have their own window are listed in this list .

To overcome this limitation, you need to use a special type of layout - the event area. On the Glade palette, it looks like this
.
It works as follows: the event area is placed in the window, and a label is placed in the event area. Event handlers are assigned to the event area. As a result, all the necessary events will be intercepted.

It is necessary to reflect this in self.init_board (), at the same time we will add an event handler for event areas:
     def init_board (self):
         # dictionary storage labels of the playing field
         self.board_widgets = {}
         # we will add tags in lines 1-3
         for row in range (0,3):
             # initialize the storage line, create an empty dictionary
             # to store the contents of table cells
             self.board_widgets [row] = {}
             # we pass columns 0-2
             for column in range (0,3):
                 # create event area
                 event_box = gtk.EventBox ()
                 # create a label, its text includes the row and column number
                 label = gtk.Label ("label_% d_% d"% (column, row))
                 # make the label visible
                 label.show ()
                 # put a label in the event area
                 event_box.add (label)
                 # define event handler for a region
                 event_box.connect ("button_press_event", self.label_clicked, row, column)
                 # write all this stuff in storage
                 self.board_widgets [row] [column] = event_box                
                 # attach the event area with the label to the table
                 self.table1.attach (self.board_widgets [row] [column], 
                                    column, column + 1,
                                    row + 1, row + 2)
                 # making the event area visible
                 self.board_widgets [row] [column] .show ()    

     def label_clicked (self, widget, event, row, column):
         print "column% s, row% s"% (column, row)

In the connect function, in addition to the signal and the widget, we also specify two additional parameters — a row and a column. These parameters are read in label_clicked, and will then be used to control the game. Both the event area and the label must be made visible with show (). If you run the program, and click on the labels, in the console you will see the reaction in the form of outputting the column and row of the table in which the label is located.

The interface is almost ready, but it is inconvenient to play without marking lines on the cells. You need to add frames to the labels. GTK has a special container widget that displays a frame. In Glade, it is called “Frame”
.

You need to change the code so that the event area is located inside the frame. At the same time we remove the text output on the labels, we no longer need it.
     def init_board (self):
         # dictionary storage labels of the playing field
         self.board_widgets = {}
         # we will add tags in lines 1-3
         for row in range (0,3):
             # initialize the storage line, create an empty dictionary
             # to store the contents of table cells
             self.board_widgets [row] = {}
             # we pass columns 0-2
             for column in range (0,3):
                 # create a frame
                 frame = gtk.Frame ()
                 frame.set_shadow_type (gtk.SHADOW_ETCHED_IN)
                 # create event area
                 event_box = gtk.EventBox ()
                 # create a label
                 label = gtk.Label ()
                 # make the label visible
                 label.show ()
                 # put a label in the event area
                 event_box.add (label)
                 # define event handler for a region
                 event_box.connect ("button_press_event", self.label_clicked, row, column)
                 event_box.show ()
                 frame.add (event_box)
                 # write all this stuff in storage
                 self.board_widgets [row] [column] = frame                
                 # attach the event area with the label to the table
                 self.table1.attach (self.board_widgets [row] [column], 
                                    column, column + 1,
                                    row + 1, row + 2)
                 # making the event area visible
                 self.board_widgets [row] [column] .show ()

it turns out such a window


Formatted text output on tags


Labels can display formatted text. You can display text in a specific font, size, style, color . Add a couple of lines to init_board
                 # after label = gtk.Label ()
                 label.set_use_markup (True)
                 label.set_markup ("<span font_desc = 'Arial 64'> X </ span>")

Run the program:
.

Dialogues



In Glade there are dialogs for almost every occasion. I will show you how to make your modal dialog, based on any window.

Create a new window, go to its properties.


The arrows highlight two important properties - the modality of the window, and the associated window. Why do I need to celebrate modality, I think it is clear - we are doing a modal dialogue. The property "Associated window" indicates for which main window it will be a modal dialog.

Using widgets and layouts already known to you, I sketched the following dialogue:

widget tree:


everything is very simple, it is only necessary to note that the file is specified as the source for the image:


Save the interface and return to the code editor.

The first thing to do is to set an event handler for the delete-event dialog. If this is not done, after closing the dialog with the X button in the window title (or in any other way through the window manager) all the widgets of this window are “annihilated”. The next time you open this window, it will be microscopic, and completely empty. So, in __init__ we add
         self.winner_dialog = self.widgetsTree.get_widget ("window2")        
         self.winner_dialog.connect ("delete-event", self.on_delete_event)

The on_delete_event method must return True to prevent further event handling.

In general, almost everything is ready, now the code of the method is interesting, which displays the winner of the game:
     def show_winner (self):
         if self.game.and_the_winner_is () == -1:
             self.widgetsTree.get_widget ("image3"). set_from_file ('1.png')
             self.widgetsTree.get_widget ("label3"). set_markup ( 
                         "<span font_desc = 'Arial 18'> Well, did you realize that I was smarter than you? </ span>")
             self.widgetsTree.get_widget ("label4"). set_text ("Yes, I am a worm and admit it.")
             self.widgetsTree.get_widget ("label5"). set_text (
                          "You're just a can, Bender, and I will prove it to you now!")        
         else:
             self.widgetsTree.get_widget ("image3"). set_from_file ('2.png')
             self.widgetsTree.get_widget ("label3"). set_markup ( 
                         "<span font_desc = 'Arial 18'> Man must lose the robot! </ span>")
             self.widgetsTree.get_widget ("label4"). set_text ("Aha, just lucky.")
             self.widgetsTree.get_widget ("label5"). set_text ("Are you small? Well, hold on!")
         self.winner_dialog.show ()

The set_from_file method is used to specify an image file, set_markup for a label indicates markup, self.winner_dialog.show () opens a dialog.

It remains to run the game under Windows:

and linux

Everything works under both OSs as intended!

The algorithm in case a player is weak (for example, a child), sets a move randomly, so you will have a chance to win, and Bender will lose.
Now is the time to download the game , and play with Bender.

Source: https://habr.com/ru/post/46370/


All Articles