The final section of this chapter presents some examples of reusable components which make use of the event/slot mechanism. The examples are taken from the stock components supplied with Rekall.
A Record Selection ToolThis is the combo-box control described at the start of the previous section on events and slots. To recap, it is a drop-down control that shows one entry for each record returned by the query, and can be used to navigate from one record to another. It is updated as appropriate whenever the user makes a change to a record, or if the user moves between records using, say, the tool bar or menu bar.
The first screenshot below shows the basic component, which has been created as a form component. The combobox is a choice control and has been embedded inside a container, so that we can show it inside a box; the container has the Frame style property set to Plain,Box width 1 (there is a second reason as well, described later). In addition, the X-mode of the choice control has been set to stretch so that once the component has been pasted into a form, changing the width of the container will change the width of the choice control.
The first slot to consider is the one which loads values into the choice control (note that although the choice control has been given the name select, the Display expression is empty, so that it is not loaded directly from the server database). To set up this slot, open the properties dialog for the choice control, double-click the Slots property, then click the Add button. This brings up an empty slot dialog, which is shown in the following screenshot with values filled in. The code is shown further on.
The Slot name is set (arbitrarily) to loadValues. The slot is being linked to the blocks postquery event, so that the code is executed just after the block has fetched data from the server database and the Link name is (again arbitrarily) set to postQuery. Importantly, the Object to which the slot is connected is set to getBlock() and the event to postquery. Of these, getBlock() will find the first block which encloses the choice control, so that we need not worry about having the choice control embedded in a container rather than directly inside a block. Note that since we are designing a component from scratch, the choice control is not actually inside a block at present, so the helper button is not of any use, and the Event setting must be set to postquery manually ( The events names are the real names of the events. The names that appear when the helper is used are the descriptive names which appear in the property dialogs. )
Clicking the Save button saves the link. Now, this slot must also be executed after the user makes any changes to displayed data, since this might affect the values displayed in the choice control. For this reason, the slot must also be connected to the postsync block event, which is triggered after a record is inserted, updated or deleted. Clicking the Add button allows a new link to be added, this time with name postSync, object getBlock() and event postsync. Clicking Save again saves the new link (the combobox near the top will now contain two entries, and is used to switch between multiple links). Finally, the OK button saves the slot.
The code used in this slot is shown below. Essentially, it builds a list of text strings, one for each record fetched from the block from server database and loads them into the choice control. Note that the choice control (ie., the object which this slot is part of) is the first argument to the slot function, and that the block (the form object in which the event occured that triggered the execution of this slot) is the second. The third argument is the event which occured, and will be either postquery or postsync; in this case we are not actually interested, but the argument must still be given in the code. Similarly, any arguments to the event are gathered together as a python list in the fourth argument, but are also ignored here.
def slotFunc (choice, block, event, *args) : list = [] for rowno in (range(block.getNumRows())) : list.append (choice.getParent().getRowValue("selector_display", rowno)) choice.setValues (list) |
The question arises, how will the code pick up the actual values that are to be displayed in the choice control? To do this, the container has a hidden control which is given the name selector_display. In addition, the hidden control has a configuration setting which provides a prompt for the Display expression property of the hidden control. So, when the component is pasted into a form, the user will have to provide an SQL expression which specifies what value the choice control will display for each record retrieved from the server database ( What would be really nice would be a way for the component selection dialog to show the user a list of table columns that are valid in the block into which the selector is being pasted. We are thinking about this! ) ; this is then substituted into the pasted component. When the form executes, the SQL query used to retrieve from the server database will include the SQL expression, and the values so retrieved are "displayed" in the hidden control (the user does not see them since the control is hidden, but they are still there). The slot code then picks up these values (getRowValue("selector_display", rowno) and loads them into the choice control. Note that the hidden control has the No Update property set so that the form does not use it in an update or insert query.
By the way, this is the other reason for using a container. This means that if the user pastes the component into a form, and then later removes is, removing the container removes both the choice control and the hidden control, so all trace of the component is eradicated. Had a container not been used, then the choice control and the hidden control would be placed directly into the component; after pasting, removing the choice control would leave the hidden control behind in the form.
We now add a second slot, which is connected to the block's On Current event. This is used so that when the user moves between records, for instance by using the navigation buttons on the toolbar, or the cursor keys, the choice control is changed to show the corresponding value. In this case, the slot is named onCurrent, while the (single) link is named onCurrent, and is connected to the object getBlock() and to the oncurrent event. The code is shown below.
def slotFunc (choice, block, event, *args) : row = block.getQueryRow() if row >= block.getNumRows() : choice.setCurrentItem (row, 0) else : choice.setCurrentItem (row, row + 1) |
When executed, the code gets the current query row number from the block. If this is greater than the total number of records in the block then the user must be adding a new record, and the choice control is set to show item zero (which will be blank), otherwise (and normally) it shows the corresponding entry (the addition of one is needed since the block numbers records from zero, but these appear in the choice control starting at item one).
Next, we need to arrange that when the user changes the current selection in the choice control, the block moves to the corresponding record. This is simply done with the On change property of the choice control:
def eventFunc (ctrl, row, value) : item = ctrl.currentItem(row) if item >= 1 : ctrl.getBlock().gotoQueryRow(item - 1) |
This component will work, but it has one shortcoming; when the user starts a query (say, by using the start database query tool on the toolbar), it would be nice if the selection component was disabled, and was re-enabled when the user either executes the query or cancels the query. The first stage to doing this is to add a slot to the container itself (rather than the choice control), connected to the On Action event of the block. The On Action event is triggered whenever a block-level event such as First Record Previous Record, etc., action occurs. In this case we are interested in the query actions. The code is shown below:
def slotFunc (ctrl, block, event, *args) : row = block.getQueryRow() if int(args[0]) == block.actQuery : ctrl.setEnabled(0) return if int(args[0]) == block.actExecute : ctrl.setEnabled(1) return if int(args[0]) == block.actCancel : ctrl.setEnabled(1) return |
The code checks the action, which is the first of the arguments to the event (and hence appears in args[0]; it is passed as a string rather than a number to the code and then converted), and looks for actQuery (user starts a query), actExecute (user executes the query), and actCancel (user cancels the query). In the first of these, the container (which is passed as the ctrl argument) is disabled; in the other two cases it is enabled. Note that the action codes can be accessed as attributes of the block, which is passed as the second argument ( Prior to version 2.0.0 of Rekall, these codes were available via the RekallMain module. This is still true of version 2.0.0, but they can now be accessed via a block object. )
If this is executed, you would notice that after the user cancels a query, the choice control is left blank, rather than showing the correct record. This is because there is no On Current event following the cancel. To get round this, we need to add one further slot to the choice control, this time connected to the block On Action event. The code for this is shown below, and is basically the same as the On Current slot, except that it only does something if the action is cancel.
def slotFunc (ctrl, block, event, *args) : if int(args[0]) == block.actCancel : row = block.getQueryRow() if row >= block.getNumRows() : ctrl.setCurrentItem (row, 0) else : ctrl.setCurrentItem (row, row + 1) |
Note that this is an example where two slots in different objects are connected to the same event on the same object.