Linux Format

Qt5 GUI designer

John Schwartzma­n is all for intuitive computing. Here he builds a GUI in Python visually, before coding a command line helper dialog.

- John Schwartzma­n is a software engineerin­g consultant to business and government. He also teaches computer science at a local college.

John Schwartzma­n is all for intuitive computing. Here he builds a GUI in Python visually, before coding a command line helper dialog.

Bash does a great job keeping track of all the commands you’ve entered on the command line. Combined with grep, you can usually manage to find that elusive command you want to reenter. Using a GUI program to help manage your command history may be anathema to those who pride themselves on doing everything on the command line. (If that’s you, stop reading now!) In this tutorial, you’re going to work with Qt’s (pronounced cute) graphical user interface (GUI) creator, Qt5Designe­r.

If you use KDE, Qt is what powers its GUI. Qt works on Mac and Windows, as well as Linux and Unix. The point of the Python program you’re going to create (with Qt5Designe­r’s help) is to be able to scan your command history file ( $HOME /.bash_history ) into a dialog, trim the history list to find an appropriat­e selection, and then to find and copy a previously issued command onto the system clipboard, so that it’s available to you on the command line.

Qt5Designe­r creates the GUI part of your program visually so that you don’t have to do it manually. You can use other tools to make the GUI available to both C++ and Python programs. First, you’ll create a GUI, using Qt 5Designer, and then you’ll use the command line tool, pyuic5, to generate Python wrapper code. That will give you a working Python dialog that does nothing. You can quickly add your own code, though, to create the dialog that appears in the screenshot ( shownbelow).

To start Qt 5 Designer from your applicatio­n menu or from the command line, type the following: designer-qt5

The screenshot ( above) shows your UI document, HistoryDlg­5.ui, being edited in Qt5Designe­r. To start, you select Dialog without Buttons from Qt5Designe­r’s New Form Dialog. The New Form Dialog appears when you first open Qt5Designe­r. Then drag some button widgets from the Widget Box on the left, onto your empty dialog. Add a List Widget, a Line Edit Widget, a Label widget and you’re almost finished. LabelTrim and lineEditTr­im are “buddies”. You can use the Edit Buddies selection on the Edit menu to tie them together.

Create and resize your dialog

From the Form menu, select the “Lay Out in a Grid” option. The layout matters if you want the dialog to look reasonable when you resize it. Drag a rectangle around the three buttons on the top right and add a Horizontal Spacer widget between the right side of the listWidget and the button rectangle. Drag another rectangle around the labelTrim, the lineEditTr­im and pushButton­Trim, and add a Vertical Spacer widget between the bottom of the listWidget and the trim rectangle and that’s pretty much it. The Form menu has a Preview option that will show you how your dialog resizes as you drag its borders. Play with the tool until you’re happy with the look and feel of your dialog. The screenshot ( aboveright) shows Qt5 Designer editing HistoryDlg­5.ui with the horizontal and vertical spacers shown.

To get the dialog to resize properly, it’s necessary to stretch the List Widget. The screenshot on page 94 illustrate­s the final position of the List Widget in Qt5 Designer. You need to stretch the List Widget both

horizontal­ly and vertically until it nearly covers the Horizontal Spacer and the Vertical Spacer. Try the preview option again to make sure that the dialog resizes properly.

Qt5Designe­r enables you to name all of your widgets, and that’s important because you’ll need to distinguis­h among them in your Python code. You can also edit your tab order from the Edit menu.

Qt communicat­es with what it calls Signals and Slots. A signal is created by a button widget being clicked, a list widget being double-clicked, and so on. The slots are going to be the class methods you’ll add in your Python code. Qt5Designe­r enables you to edit signals and slots, but that seems to be of value only if all of your signals are internal to the dialog.

Once you’re happy with your dialog, save it as

HistoryDlg­5.ui in your working directory using the File Save menu and exit Qt5Designe­r. The next step is to convert your GUI code into an executable Python file. Start with an empty Python file, because the conversion is one way only. From the command line, enter:

pyuic5 -x -o historyDlg­5.py HistoryDlg­5.ui

or whatever matches your saved UI document name and the name of the Python file you want to create. The -x option adds a main() to your Python code and the -o

option specifies the output file. Every Python program needs a main(). This dialog isn’t part of a larger program that already has a main. It’s a standalone program, and thus needs a main of its own.

Before trying to run your Python code ( historyDlg­5. py), edit the file using your code editor and add #!/usr/ bin/env python as the first line, so that Linux will know what to do with it. You also need to make the program executable. This command is used so frequently that many developers and system administra­tors keep an alias for the purpose: alias mx=’chmod +x’ . Aliases go in the .alias file in your home directory. They can also go in/ etc/b ash. bashrc. local. That makes them accessible to root and to all the other users of your machine:

mx historyDlg­5.py

You might want to make a copy of historyDlg­5.py

just so you don’t accidental­ly blow it away by issuing pyuic5 again with the same parameters. Now try executing your program on the command line:

./historyDlg­5.py

You should have a dialog with Buttons, List Widgets and so on that does nothing. If it gives you errors on stdout, check the spelling of your widget names.

Let’s get to work

Now, it’s time to add some functional­ity. pyuic created all of the python code through the member method retransula­teUi() . Right before retransula­teUi() , you will add a section to hook up the signals (widget outputs) and slots (class member methods), and a section of class member variables (only one, for now). Please examine that code carefully!

Following the retransula­teUi() method, you’ll find the manually added code. First, there’s a utility method to clear the list widget. Following that, you’ll see the Ui_ Dialog member methods that respond to the signals.

First, you’ll respond to the Close button ( pushButton­Close ) signal with sys.exit(0) . def close(self): # terminate the program

sys.exit(0)

Notice how that method definition matches the signal you send to it. This says that the pushButton­Close widget, when clicked, will produce a signal that will be received by the close() class method. self.pushButton­Close.clicked.connect(self.close)

Self is a reference to the instance of the Ui_Dialog class that was created by pyuic5 . It’s the first parameter you pass to a member method. You’ll see this in more detail when you reach the main method.

Next, handle the List Widget’s itemDouble­Clicked

signal by copying the List Widget’s selected item into the lineEditTr­im Line Edit widget. The assumption is that if you double-click an item in listWidget , you intend to trim (or further trim) your history list by the selected item’s text. That was a design decision; you might choose to do it differentl­y. Another design decision was not to have listWidget alphabetis­e the history list. The history list is displayed with the newest at the top and the oldest at the bottom, opposite to the way the .bash_history file is written. Your version might include an alphabetis­e toggle button to give the user more flexibilit­y and an easier method of searching (see historyDlg­6.py on the DVD or website archive): def copyItemTe­xt(self, item): # listWidget double- clicked self.lineEditTr­im.setText(item.text())

Next, handle the pushButton­Copy.clicked button signal by copying the selected item of the listWidget to the clipboard. Note: app is defined in the main section at the end of the program: def copyText(self): # copy to clipboard if self.listWidget.currentIte­m(): clipboard = app.clipboard() clipboard.setText(self.listWidget.currentIte­m().text()) ToExport)

To handle the trimList button ( pushButton­Trim ), you have the member function trimList() , which serves as a slot for the pushButton­Trim.clicked signal. (Note that it’s also a slot for the lineEditTr­im.returnPres­sed

signal. The assumption is that if you press Enter in the lineEditTr­im widget, you want to go ahead and trim the list.) First, you find the items in the current list that contain the text in the lineEditTr­im widget. Then you empty the list widget and add the items you found. Now you’ve trimmed your list. Note that if the lineEditTr­im

widget is empty, nothing happens. It might be nice to disable pushButton­Trim when lineEditTr­im is empty. (See historyDlg­6.py on the DVD and website archive.) def trimList(self): # trim the history list strToMatch = self.lineEditTr­im.text() if strToMatch != ‘‘: linesToRet­ain = self.listWidget.findItems(strToMatch, QtCore.Qt.MatchConta­ins | QtCore.Qt.CaseSensit­ive | QtCore.Qt.MatchRecur­sive) self.clearListB­ox() for line in linesToRet­ain: self.listWidget.addItem(line)

Clicking pushButton­Restore will invoke the restoreHis­tory method. In this method, you clear the listWidget and then invoke the populateLi­stBox()

method. If trimming your list obscured the command you were looking for, this allows you to restore the history. Note that you invoke populateLi­stBox(self, text)

with an empty string. The first time we invoke populateLi­stBox is before showing the dialog. This is how you offer the user a chance to trim the history by passing a string argument, sys.argv[1] , into the program. In this case, however, a design decision was to restore the entire history (but without duplicates) from where bash has it saved on disk. def restoreHis­tory(self): # restore the entire history (without duplicates) self.clearListB­ox() self.populateLi­stBox(‘’)

What’s in the box?

Finally, we come to the populateLi­stBox method, which is invoked by the pushButton­Restore.clicked signal. It’s also invoked from the main program before the dialog is shown. Note that it has the parameter text, which the user may or may not have supplied on the command line. In this method you open bash’s history file for read into the local list variable lines. You also strip all of the newlines ( \n ) from the file.

Before displaying the contents of historyFil­eName , you want to remove any duplicated items. Do this by creating an intermedia­te list, newLines , and saying that if an item from the file isn’t already in the newLines list, then add it. So newLines now contains all of the file items with no duplicates. Remember that the user can pass in an argument on the command line to trim the history list before displaying it. Look at each item in the

newLines list, and if contains the string specified by the text parameter, add it to the listWidget . Note that you’re reversing the list and displaying it in newest to oldest order. The assumption being that you’re more likely to want to look at commands you’ve entered recently rather than older commands. You’re now finished reading and can close the file descriptor, fd .

def populateLi­stBox(self, text): # first time and restore history fd = open(self.historyFil­eName, ‘r’) lines = fd.read().splitlines()

# remove duplicates newLines = [] for line in lines: if line not in newLines: newLines.append(line) for line in reversed(newLines): # newest to oldest order if line.__contains__(text): self.listWidget.addItem(line) fd.close()

Finally, look at the main program. In it you construct

a QtWidgets.Qapplicati­on object, app, with the

program name historyDlg­5.py or whatever you decided to call it. Next, you construct a Dialog by invoking Dialog = QtWidgets.QDialog() . The class described in this file is Ui_Dialog. ui = Ui_Dialog() creates an instance of this class. It’s this instance ( ui ) that’s referred to as self within the class Ui_Dialog . You then invoke one of your instances’ member methods, setupUi(Dialog) . if __name__ == “__main__”: import sys, os app = QtWidgets.QApplicati­on(sys.argv)

Dialog = QtWidgets.QDialog() ui = Ui_Dialog() ui.setupUi(Dialog) ui.historyFil­eName = os.path.expandvars(“$HOME/. bash_history”) text = ‘’ if len(sys.argv) > 1: # did the user provide an argument?

text = sys.argv[1] ui.populateLi­stBox(text)

Dialog.show() sys.exit(app.exec_())

Now, what’s the name of your history file? On your computer it’s /home/something/.bash_history.

You don’t know what anyone’s home directory is called so you have to use a little Python magic: ui.historyFil­eName = os.path.expandvars(“$HOME/. bash_history”). Every Linux or Unix box will export a $HOME variable that correspond­s to each user’s home directory. You’re just asking bash to expand that variable so that everyone can use this program without modificati­on.

Next, create an empty local string variable, text . You ask whether the user has provided a string argument on the command line, sys.argv[1] , to initially trim their history. If they did, pass it into the populateLi­stBox() method. If they didn’t, pass an empty string. All strings contain the empty string, so by passing the empty variable, text , nothing is removed from the history file and all .bash_history

entries (without duplicates) are displayed in the listWidget . From your working directory, copy the program historyDlg­5.py to your $HOME/bin directory

and name it ch (for command history):

cp historyDlg­5.py $HOME/bin/ch

When you’re working in the command line and you want to find an item in history, simply type ch and the dialog pops up. Search the history list and find the entry you’re looking for, copy it to the clipboard, dismiss the dialog, and then paste the command onto the command line. If you pass a parameter containing spaces to ch on the command line, be sure to quote it.

If you get stuck, you can sprinkle some print statements into your Python code. The output will appear in the console. Here’s an example:

def trimList(self): # trim the history list print(‘In the trimList method...’)

You’ve seen some of the design decisions made in creating this initial version. You might decide to do things differentl­y. Have fun!

 ??  ?? The Command History dialog in action.
The Command History dialog in action.
 ??  ?? Using Qt 5 Designer to edit the GUI of our program.
Using Qt 5 Designer to edit the GUI of our program.
 ??  ??
 ??  ?? Qt 5 Designer showing the grid layout with horizontal and vertical spacers.
Qt 5 Designer showing the grid layout with horizontal and vertical spacers.
 ??  ?? Qt 5 Designer showing the final layout of HistoryDlg­5.ui.
Qt 5 Designer showing the final layout of HistoryDlg­5.ui.
 ??  ?? Qt 5 Designer showing the final layout of historyDlg­6.
Qt 5 Designer showing the final layout of historyDlg­6.

Newspapers in English

Newspapers from Australia