Qt5 GUI designer
John Schwartzman is all for intuitive computing. Here he builds a GUI in Python visually, before coding a command line helper dialog.
John Schwartzman 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, Qt5Designer.
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 Qt5Designer’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 appropriate 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.
Qt5Designer 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 application menu or from the command line, type the following: designer-qt5
The screenshot ( above) shows your UI document, HistoryDlg5.ui, being edited in Qt5Designer. To start, you select Dialog without Buttons from Qt5Designer’s New Form Dialog. The New Form Dialog appears when you first open Qt5Designer. 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 lineEditTrim 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 lineEditTrim and pushButtonTrim, 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 HistoryDlg5.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 illustrates the final position of the List Widget in Qt5 Designer. You need to stretch the List Widget both
horizontally 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.
Qt5Designer enables you to name all of your widgets, and that’s important because you’ll need to distinguish among them in your Python code. You can also edit your tab order from the Edit menu.
Qt communicates 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. Qt5Designer 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
HistoryDlg5.ui in your working directory using the File Save menu and exit Qt5Designer. 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 historyDlg5.py HistoryDlg5.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 ( historyDlg5. 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 administrators 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 historyDlg5.py
You might want to make a copy of historyDlg5.py
just so you don’t accidentally blow it away by issuing pyuic5 again with the same parameters. Now try executing your program on the command line:
./historyDlg5.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 functionality. pyuic created all of the python code through the member method retransulateUi() . Right before retransulateUi() , 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 retransulateUi() 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 ( pushButtonClose ) 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 pushButtonClose widget, when clicked, will produce a signal that will be received by the close() class method. self.pushButtonClose.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 itemDoubleClicked
signal by copying the List Widget’s selected item into the lineEditTrim 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 differently. Another design decision was not to have listWidget alphabetise 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 alphabetise toggle button to give the user more flexibility and an easier method of searching (see historyDlg6.py on the DVD or website archive): def copyItemText(self, item): # listWidget double- clicked self.lineEditTrim.setText(item.text())
Next, handle the pushButtonCopy.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.currentItem(): clipboard = app.clipboard() clipboard.setText(self.listWidget.currentItem().text()) ToExport)
To handle the trimList button ( pushButtonTrim ), you have the member function trimList() , which serves as a slot for the pushButtonTrim.clicked signal. (Note that it’s also a slot for the lineEditTrim.returnPressed
signal. The assumption is that if you press Enter in the lineEditTrim 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 lineEditTrim widget. Then you empty the list widget and add the items you found. Now you’ve trimmed your list. Note that if the lineEditTrim
widget is empty, nothing happens. It might be nice to disable pushButtonTrim when lineEditTrim is empty. (See historyDlg6.py on the DVD and website archive.) def trimList(self): # trim the history list strToMatch = self.lineEditTrim.text() if strToMatch != ‘‘: linesToRetain = self.listWidget.findItems(strToMatch, QtCore.Qt.MatchContains | QtCore.Qt.CaseSensitive | QtCore.Qt.MatchRecursive) self.clearListBox() for line in linesToRetain: self.listWidget.addItem(line)
Clicking pushButtonRestore will invoke the restoreHistory method. In this method, you clear the listWidget and then invoke the populateListBox()
method. If trimming your list obscured the command you were looking for, this allows you to restore the history. Note that you invoke populateListBox(self, text)
with an empty string. The first time we invoke populateListBox 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 restoreHistory(self): # restore the entire history (without duplicates) self.clearListBox() self.populateListBox(‘’)
What’s in the box?
Finally, we come to the populateListBox method, which is invoked by the pushButtonRestore.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 historyFileName , you want to remove any duplicated items. Do this by creating an intermediate 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 populateListBox(self, text): # first time and restore history fd = open(self.historyFileName, ‘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.Qapplication object, app, with the
program name historyDlg5.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.QApplication(sys.argv)
Dialog = QtWidgets.QDialog() ui = Ui_Dialog() ui.setupUi(Dialog) ui.historyFileName = os.path.expandvars(“$HOME/. bash_history”) text = ‘’ if len(sys.argv) > 1: # did the user provide an argument?
text = sys.argv[1] ui.populateListBox(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.historyFileName = os.path.expandvars(“$HOME/. bash_history”). Every Linux or Unix box will export a $HOME variable that corresponds to each user’s home directory. You’re just asking bash to expand that variable so that everyone can use this program without modification.
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 populateListBox() 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 historyDlg5.py to your $HOME/bin directory
and name it ch (for command history):
cp historyDlg5.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 differently. Have fun!