본문 바로가기
Mastering-GUI-Programming

Building Forms with QtWidgets

by 자동매매 2023. 3. 8.

2 Building Forms with QtWidgets

One of the first steps in application development is prototyping your app's GUI. With a wide range of ready-to-use widgets, PyQt makes this very easy. Best of all, we can move our prototype code directly into an actual application when we're done.

In this chapter, we're going to get familiar with basic form design over the following topics:

  • Creating basic QtWidgets widgets
  • Placing and arranging widgets
  • Validating widgets
  • Building a calendar application GUI

Technical requirements

To complete this chapter, you'll need everything from Chapter 1, Getting Started with PyQt, plus the example code from https://github.com/PacktPublishing/Mastering-GUI- Programming-with-Python/tree/master/Chapter02.

Check out the following video to see the code in action: http://bit.ly/2M2R26r

Creating basic QtWidgets widgets

The QtWidgets module contains dozens of widgets, some simple and standard, others complex and unique. In this section, we're going to go through eight of the most common widgets and their basic usage.

Before starting this section, make a copy of your application template from Chapter 1, Getting Started with PyQt, and save it to a file called widget_demo.py. As we go through the examples, you can add them into your MainWindow.__init__() method to see how the objects work.

Building Forms with QtWidgets Chapter 59*

QWidget

QWidget is the parent class of all other widgets, so any properties and methods it has will also be available in any other widget. By itself, a QWidget object can be useful as a container for other widgets, a filler to fill blank areas, or as a base class for top-level windows.

Creating a widget is as simple as this:

  • inside MainWindow.__init__()

subwidget = qtw.QWidget(self)

Notice we've passed self as an argument. If we're creating a widget to be placed on or used inside another widget class, as we are here, it's a good idea to pass a reference to the parent widget as the first argument. Specifying a parent widget will ensure that the child widget is destroyed and cleaned up when the parent is, and limit its visibility to inside the parent widget.

As you learned in Chapter 1, Getting Started with PyQt, PyQt also allows us to specify values for any of the widget's properties.

For example, we can use the toolTip property to set the tooltip text (which will pop up when the widget has hovered with the mouse) for this widget:

subwidget = qtw.QWidget(self, toolTip='This is my widget') Read the C++ documentation for QWidget (found at https://doc.qt.io/qt-5/qwidget.

html) and note the class's properties. Note that each property has a specified data type. In this case, toolTip requires QString. We can use a regular Unicode string whenever

QString is required because PyQt translates it for us. For more esoteric data types, such

as QSize or QColor, we would need to create the appropriate object. Be aware that these conversions are happening in the background, however, as Qt is not forgiving about data types.

For example, this code results in an error:

subwidget = qtw.QWidget(self, toolTip=b'This is my widget') This would result in TypeError because PyQt won't convert a bytes object into QString.

Because of this, make sure you check the data type required by a widget's properties or method calls and use a compatible type.

QWidget as a top-level window

When a QWidget is created without a parent and its show() method is called, it becomes a top-level window. When we use it as a top-level window, such as we do with our MainWindow instance, there are some window-specific properties we can set. Some of these are shown in the following table:

Property Argument type Description
windowTitle string The title of the window.
windowIcon QIcon The icon for the window.
modal Boolean Whether the window is modal.
cursor Qt.CursorShape The cursor used when this widget has hovered.
windowFlags Qt.WindowFlags How the OS should treat the window (dialog, tooltip, popup).
The argument type for cursor is an example of an enum. An enum is simply a list of named values, and Qt defines enum anywhere that a property is limited to a set of descriptive values. The argument for windowFlags is an example of a flag. Flags are like enums, except that they can be combined (using the pipe operator, ) so that multiple flags can be passed.

In this case, both the enum and flag are part of the Qt namespace, found in the QtCore module. So, for example, to set the cursor to an arrow cursor when the widget is hovered over, you'd need to find the right constant in Qt that refers to the arrow cursor and set the widget's cursor property to that value. To set flags on the window indicating to the OS that it's a sheet and popup window, you'd need to find the constants in Qt that represent those window flags, combine them with the pipe, and pass it as the value for windowFlags.

Creating such a QWidget window might look like this:

window = qtw.QWidget(cursor=qtc.Qt.ArrowCursor) window.setWindowFlags(qtc.Qt.Sheet|qtc.Qt.Popup)

We'll encounter many more flags and enums as we learn to configure Qt widgets throughout the rest of this book.

QLabel

QLabel is a QWidget object configured to display simple text and images. Creating one looks like this:

label = qtw.QLabel('Hello Widgets!', self)

Notice this time that the parent widget is specified as the second argument, while the first argument is the text of the label.

Some commonly used QLabel properties are shown here:

Property Argument Description
text string Text to display on the label.
margin integer Space (in pixels) around the text.
indent integer Space (in pixels) to indent the text.
wordWrap Boolean Whether to wrap long lines.
textFormat Qt.TextFormat Force plaintext or rich text, or auto-detect.
pixmap QPixmap An image to display instead of the text.
The label's text is stored in its text property so it can be accessed or changed using the related accessor methods, like this:

label.setText("Hi There, Widgets!") print(label.text())

QLabel can display plaintext, rich text, or an image. Rich text in Qt uses an HTML-like syntax; by default, the label will automatically detect whether your string contains any formatting tags and display the appropriate type of text accordingly. For example, if we wanted to make our label boldface and add a margin around the text, we could do so like this:

label = qtw.QLabel('Hello Widgets!', self, margin=10)

We will learn more about using images, rich text, and fonts in Chapter 6, Styling Qt Applications, and Chapter 11, Creating Rich Text with QTextDocument.

QLineEdit

The QLineEdit class is a single-line text-entry widget that you might commonly use in a data-entry or login form. QLineEdit can be called with no arguments, with only a parent widget, or with a default string value as the first argument, like so:

line_edit = qtw.QLineEdit('default value', self)

There are also a number of properties we can pass in:

Property Arguments Description
text string The contents of the box.
readOnly Boolean Whether the field can be edited.
clearButtonEnabled Boolean Whether a clear button is added.
placeholderText string Text that will appear when the field is empty.
maxLength integer The maximum number of characters that can be entered.
echoMode QLineEdit.EchoMode Switches the way text is displayed as its entered (such as for password entry).
Let's add some properties to our line edit widget:

line_edit = qtw.QLineEdit(

'default value', self,

placeholderText='Type here', clearButtonEnabled=True, maxLength=20

)

This will populate the widget with a default text of 'default value'. It will display a placeholder string of 'Type here' when the field is empty or a small X button that clears the field when it has text in it. It also limits the number of characters that can be typed to 20.

QPushButton and other buttons

QPushButton is a simple, clickable button widget. Like QLabel and QLineEdit, it can be called with a first argument that specifies the text on the button, like so:

button = qtw.QPushButton("Push Me", self)

Some of the more useful properties we can set on QPushButton include the following:

Property Arguments Description
checkable Boolean Whether the button stays on when pressed.
checked Boolean For checkable buttons, whether the button is checked.
icon QIcon An icon image to display on the button.
shortcut QKeySequence A keyboard shortcut that will activate the button.
The checkable and checked properties allow us to use this button as a toggle button that reflects an on/off state, rather than just a click button that performs an action. All of these properties come from the QPushButton class's parent class, QAbstractButton. This is also

the parent class of several other button classes, listed here:

Class Description
QCheckBox A checkbox can be Boolean for on/off or tristate for on/partially on/off.
QRadioButton Like checkbox, but only one button among those with the same parent can be checked.
QToolButton Special button for use on toolbar widgets.
Though each has some unique features, for the core functionality, these buttons are the same in terms of how we create and configure them.

Let's make our button checkable, check it by default, and give it a shortcut:

button = qtw.QPushButton(

"Push Me",

self,

checkable=True, checked=True, shortcut=qtg.QKeySequence('Ctrl+p') )

Note that the shortcut option requires us to pass in a QKeySequence, which is part of the QtGui module. This is a good example of how property arguments often need to be wrapped in some kind of utility class. QKeySequence encapsulates a key combination, in this case, the Ctrl key (or command key, on macOS) and P.

Key sequences can be specified as a string, such as the preceding example, or by using enum values from the QtCOre.Qt module. For example, we could write the preceding as QKeySequence(qtc.Qt.CTRL +

qtc.Qt.Key_P).

QComboBox

A combobox, also known as a dropdown or select widget, is a widget that presents a list of options when clicked on, one of which must be selected. QCombobox can optionally allow text input for custom answers by setting its editable property to True.

Let's create a QCombobox object like so:

combobox = qtw.QComboBox(self)

Right now, our combobox has no items in its menu. QCombobox doesn't provide a way to initialize the widget with options in the constructor; instead, we have to create the widget, then use the addItem() or insertItem() method to populate its menu with options, like

so:

combobox.addItem('Lemon', 1)

combobox.addItem('Peach', 'Ohh I like Peaches!') combobox.addItem('Strawberry', qtw.QWidget)

combobox.insertItem(1, 'Radish', 2)

The addItem() method takes a string for the label and a data value. As you can see, this value can be anything—an integer, a string, a Python class. This value can be retrieved for the currently selected item using the QCombobox object's currentData() method. It's typically a good idea—though not required—to make all the item values be of the same type.

addItem() will always append items to the end of the menu; to insert them earlier, use the insertItem() method. It works exactly the same, except that it takes an index (integer

value) for the first argument. The item will be inserted at that index in the list. If we want to save time and don't need a data property for our items, we can also use addItems() or insertItems() to pass in a list of options.

Some other important properties for QComboBox include the following:

Property Arguments Description
currentData (anything) The data object of the currently selected item.
currentIndex integer The index of the currently selected item.
currentText string The text of the currently selected item.
editable Boolean Whether combobox allows text entry.
insertPolicy QComboBox.InsertPolicy Where entered items should be inserted in the list.
The data type for currentData is QVariant, a special Qt class that acts as a container for any kind of data. These are more useful in C++, as they provide a workaround for static typing in situations where multiple data types might be useful. PyQt automatically converts QVariant objects to

the most appropriate Python type, so we rarely need to work directly with this type.

Let's update our combobox so that we can add items to the top of the dropdown:

combobox = qtw.QComboBox( self,

editable=True, insertPolicy=qtw.QComboBox.InsertAtTop )

Now this combobox will allow any text to be typed in; the text will be added to the top of the list box. The data property for the new items will be None, so this is really only appropriate if we are working with the visible strings only.

QSpinBox

In general, a spinbox is a text entry with arrow buttons designed to spin through a set of incremental values. QSpinbox is built specifically to handle either integers or discrete values (such as a combobox).

Some useful QSpinBox properties include the following:

Property Arguments Description
value integer The current spinbox value, as an integer.
cleanText string The current spinbox value, as a string (excludes the prefix and suffix).
maximum integer The maximum integer value of the box.
minimum integer The minimum value of the box.
prefix string A string to prepend to the displayed value.
suffix string A string to append to the displayed value.
singleStep integer How much to increment or decrement the value when the arrows are used.
wrapping Boolean Whether to wrap from one end of the range to the other when the arrows are used.
Let's create a QSpinBox object in our script, like this:

spinbox = qtw.QSpinBox( self, value=12, maximum=100, minimum=10, prefix='$',

suffix=' + Tax', singleStep=5 )

This spinbox starts with a value of 12 and will allow entry of integers from 10 to 100, displayed in the $ + Tax format. Note that the non-integer portion of the box is

not editable. Also note that, while the increment and decrement arrows move by 5, nothing prevents us from entering a value that is not a multiple of 5.

QSpinBox will automatically ignore keystrokes that are not numeric, or that would put the value outside the acceptable range. If a value is typed that is too low, it will be auto- corrected to a valid value when the focus moves from the spinbox; for example, if you typed 9 into the preceding box and clicked out of it, it would be auto-corrected to 90.

QDoubleSpinBox is identical to QSpinBox, but designed for a decimal or floating-point numbers.

To use QSpinBox for discrete text values instead of integers, you need to subclass it and override its validation methods. We'll do that later in the Validating widgets section.

QDateTimeEdit

A close relative of the spinbox is QDateTimeEdit, designed for entering date-time values.

By default, it appears as a spinbox that allows the user to tab through each field in the date- time value and increment/decrement it using the arrows. The widget can also be configured to use a calendar popup.

The more useful properties include the following:

Property Arguments Description
date QDate or datetime.date The date value.
time QTime or datetime.time The time value.
dateTime QDateTime or datetime.datetime The combined date-time value.
maximumDate, minimumDate QDate or datetime.date The maximum and minimum date enterable.
maximumTime, minimumTime QTime or datetime.time The maximum and minimum time enterable.

,

maximumDateTime minimumDateTime

QDateTime or datetime.datetime The maximum and minimum date-time enterable.
calendarPopup Boolean Whether to display the calendar popup or behave like a spinbox.
displayFormat string How the date-time should be formatted.
Let's create our date-time box like this:

datetimebox = qtw.QDateTimeEdit( self, date=qtc.QDate.currentDate(),

time=qtc.QTime(12, 30), calendarPopup=True,

maximumDate=qtc.QDate(2030, 1, 1), maximumTime=qtc.QTime(17, 0),

displayFormat='yyyy-MM-dd HH:mm' )

This date-time widget will be created with the following attributes:

  • It will be set to 12:30 on the current date
  • It will show the calendar popup when focused
  • It will disallow dates after January 1st, 2030
  • It will disallow times after 17:00 (5 PM) on the maximum date
  • It will display date-times in the year-month-day hour-minutes format

Note that maximumTime and minimumTime only impact the maximumDate and

minimumDate values, respectively. So, even though we've specified a maximum time of 17:00, nothing prevents you from entering 18:00 as long as it's before January 1st, 2030. The same concept applies to minimum dates and times.

The display format for the date-time is set using a string that contains specific substitution codes for each item. Some of the more common codes are listed here:

Code Meaning
d Day of the month.
M Month number.
yy Two-digit year.
yyyy Four-digit year.
h Hour.
m Minute.
s Second.
A AM/PM, if used, hour will switch to 12-hour time.
Day, month, hour, minute, and second all default to omitting the leading zero. To get a leading zero, just double up the letter (for example, dd for a day with a leading zero). A complete list of the codes can be found at https://doc.qt.io/qt-5/qdatetime.html.

Note that all times, dates, and date-times can accept objects from the Python standard library's datetime module as well as the Qt types. So, our box could just as well have been created like this:

import datetime

datetimebox = qtw.QDateTimeEdit( self, date=datetime.date.today(),

time=datetime.time(12, 30), calendarPopup=True,

maximumDate=datetime.date(2020, 1, 1), minimumTime=datetime.time(8, 0),

maximumTime=datetime.time(17, 0),

displayFormat='yyyy-MM-dd HH:mm' )

Which one you choose to use is a matter of personal preference or situational requirements. For instance, if you are working with other Python modules, the datetime standard library objects are going to be more compatible. If you just need to set a default value for a widget, QDateTime may be more convenient, since you likely already have QtCore imported.

If you need more control over the date and time entry, or just want to split these up, Qt has the QTimeEdit and QDateEdit widgets. They're just like

this widget, except they only handle time and date, respectively.

QTextEdit

While QLineEdit exists for single-line strings, QTextEdit provides us with the capability

to enter multi-line text. QTextEdit is much more than just a simple plaintext entry, though; it's a full-blown WYSIWYG editor that can be configured to support rich text and images.

Some of the more useful properties of QTextEdit are shown here:

Property Arguments Description
plainText string The contents of the box, in plaintext.
html string The contents of the box, as rich text.
acceptRichText Boolean Whether the box allows rich text.
lineWrapColumnOrWidth integer The pixel or column at which the text will be wrapped.
lineWrapMode QTextEdit.LineWrapMode Whether the line wrap uses columns or pixels.
overwriteMode Boolean Whether overwrite is activated; False means insert mode.
placeholderText string Text to display when the field is empty.
readOnly Boolean Whether the field is read-only.
Let's create a text edit like this:

textedit = qtw.QTextEdit(

self,

acceptRichText=False, lineWrapMode=qtw.QTextEdit.FixedColumnWidth, lineWrapColumnOrWidth=25,

placeholderText='Enter your text here' )

This will create a plaintext editor that only allows 25 characters to be typed per line, with the phrase 'Enter your text here' displayed when it's empty.

We'll dig in deeper to the QTextEdit and rich text documents in Chapter 11, *Creating Rich Text with QTextDocument.

Placing and arranging widgets

So far, we've created a lot of widgets, but if you run the program you won't see any of them. Although our widgets all belong to the parent window, they haven't been placed on it yet. In this section, we'll learn how to arrange our widgets in the application window and set them to an appropriate size.

Layout classes

A layout object defines how child widgets are arranged on a parent widget. Qt offers a variety of layout classes, each of which has a layout strategy appropriate for different situations.

The workflow for using layout classes goes like this:

  1. Create a layout object from an appropriate layout class
  2. Assign the layout object to the parent widget's layout property using the setLayout() method
  3. Add widgets to the layout using the layout's addWidget() method

You can also add layouts to a layout using the addLayout() method to create more complex arrangements of widgets. Let's take a tour of a few of the basic layout classes offered by Qt.

QHBoxLayout and QVBoxLayout

QHBoxLayout and QVBoxLayout are both derived from QBoxLayout, a very basic layout engine that simply divides the parent into horizontal or vertical boxes and places widgets sequentially as they're added. QHBoxLayout is oriented horizontally, and widgets are placed from left to right as added. QVBoxLayout is oriented vertically, and widgets are placed from top to bottom as added.

Let's try QVBoxLayout on our MainWindow widget:

layout = qtw.QVBoxLayout() self.setLayout(layout)

Once the layout object exists, we can start adding our widgets to it using the addWidget() method:

layout.addWidget(label) layout.addWidget(line_edit)

As you can see, if you run the program, the widgets are added one per line. If we wanted to add several widgets to a single line, we could nest a layout inside our layout, like this:

sublayout = qtw.QHBoxLayout() layout.addLayout(sublayout)

sublayout.addWidget(button) sublayout.addWidget(combobox)

Here, we've added a horizontal layout to the next cell of our main vertical layout and then inserted three more widgets to the sub-layout. These three widgets display side by side in a single line of the main layout. Most application layouts can be accomplished by simply nesting box layouts in this manner.

QGridLayout

Nested box layouts cover a lot of ground, but in some situations, you might like to arrange widgets in uniform rows and columns. This is where QGridLayout comes in handy. As the name suggests, it allows you to place widgets in a table-like structure.

Create a grid layout object like this:

grid_layout = qtw.QGridLayout() layout.addLayout(grid_layout)

Adding widgets to QGridLayout is similar to the method for the QBoxLayout classes, but also requires passing coordinates:

grid_layout.addWidget(spinbox, 0, 0)

grid_layout.addWidget(datetimebox, 0, 1)

grid_layout.addWidget(textedit, 1, 0, 2, 2) Here are the arguments for QGridLayout.addWidget(), in order:

  1. The widget to add
  2. The row number (vertical coordinate), starting from 0
  3. The column number (horizontal coordinate), starting from 0
  4. The row span, or how many rows the widget will encompass (optional)
  5. The column span, or how many columns the widget will encompass (optional)

Thus, our spinbox widget is placed at row 0, column 0, which is the top left; our datetimebox at row 0, column 1, which is the top right; and our textedit at row 1, column 0, and it spans two rows and two columns.

Keep in mind that the grid layout keeps consistent widths on all columns and consistent heights on all rows. Thus, if you place a very wide widget in row 2, column 1, all widgets in all rows that happen to be in column 1 will be stretched accordingly. If you want each cell to stretch independently, use nested box layouts instead.

QFormLayout

When creating data-entry forms, it's common to have labels next to the input widgets they label. Qt provides a convenient two-column grid layout for this situation

called QFormLayout.

Let's add a form layout to our GUI:

form_layout = qtw.QFormLayout() layout.addLayout(form_layout)

Adding widgets can be easily done with the addRow() method:

form_layout.addRow('Item 1', qtw.QLineEdit(self))

form_layout.addRow('Item 2', qtw.QLineEdit(self))

form_layout.addRow(qtw.QLabel('This is a label-only row'))

This convenience method takes a string and a widget and automatically creates the QLabel widget for the string. If passed only a single widget (such as a QLabel), the widget spans both columns. This can be useful for headings or section labels.

QFormLayout is not just a mere convenience over QGridLayout, it also automatically

provides idiomatic behavior when used across different platforms. For example, when used on Windows, the labels are left-justified; when used on macOS, the labels are right-justified, keeping with the design guidelines of the platform. Additionally, when viewed on a narrow screen (such as a mobile device), the layout automatically collapses to a single column with the labels above the input. It's definitely worthwhile to use this layout any time you have a two-column form.

Controlling widget size

If you run our demo as it currently is and expand it to fill your screen, you'll notice that each cell of the main layout gets evenly stretched to fill the screen, as shown here:

This isn't ideal. The label at the top really doesn't need to be expanded, and there is a lot of wasted space at the bottom. Presumably, if a user were to expand this window, they'd do so to get more space in input widgets like our QTextEdit. We need to give the GUI some guidance on how to size our widgets, and how to resize them in the event that the window is expanded or shrunk from its default size.

Controlling the size of widgets can be a bit perplexing in any toolkit, but Qt's approach can be especially confusing, so let's take it one step at a time.

We can simply set a fixed size for any widget using its setFixedSize() method, like this:

  • Fix at 150 pixels wide by 40 pixels high

label.setFixedSize(150, 40)

setFixedSize accepts only pixel values, and a widget set to a fixed size cannot be altered from those pixel sizes under any circumstances. The problem with sizing a widget this way is that it doesn't account for the possibility of different fonts, different text sizes, or changes to the size or layout of the application window, which might result in the widget being too small for its contents, or needlessly large. We can make it slightly more flexible by

setting minimumSize and maximumSize, like this:

  • setting minimum and maximum sizes

line_edit.setMinimumSize(150, 15)

line_edit.setMaximumSize(500, 50)

If you run this code and resize the window, you'll notice line_edit has a bit more flexibility as the window expands and contracts. Note, however, that the widget won't shrink below its minimumSize, but it won't necessarily use its maximumSize, even if the room is available.

So, this is still far from ideal. Rather than concern ourselves with how many pixels each widget consumes, we'd prefer it be sized sensibly and fluidly with respect to its contents and role within the interface. Qt does just this using the concepts of size hints and size polices.

A size hint is a suggested size for a widget and is returned by the widget's sizeHint() method. This size may be based on a variety of dynamic factors; for example, the QLabel widget's sizeHint() value depends on the length and wrap of the text it contains. Because it's a method and not a property, setting a custom sizeHint() for a widget requires you to subclass the widget and reimplement the method. Fortunately, this isn't something we often need to do.

A size policy defines how the widget responds to a resizing request with respect to its size hint. This is set as the sizePolicy property of a widget. Size policies are defined in the QtWidgets.QSizePolicy.Policy enum, and set separately for the horizontal and

vertical dimensions of a widget using the setSizePolicy accessor method. The available policies are listed here:

Policy Description
Fixed Never grow or shrink.
Minimum Don't get smaller than sizeHint. Expanding isn't useful.
Maximum Don't get larger than sizeHint, shrink if necessary.
Policy Description
Preferred Try to be sizeHint, but shrink if necessary. Expanding isn't useful. This is the default.
Expanding Try to be sizeHint, shrink if necessary, but expand if at all possible.
MinimumExpanding Don't get smaller than sizeHint, but expand if at all possible.
Ignored Forget sizeHint altogether, just take up as much space as possible.
So, for example, if we'd like the spinbox to stay at a fixed width so the widget next to it can expand, we would do this:

spinbox.setSizePolicy(qtw.QSizePolicy.Fixed,qtw.QSizePolicy.Preferred) Or, if we'd like our textedit widget to fill as much of the screen as possible, but never

shrink below its sizeHint() value, we should set its policies like this:

textedit.setSizePolicy( qtw.QSizePolicy.MinimumExpanding, qtw.QSizePolicy.MinimumExpanding )

Sizing widgets can be somewhat unpredictable when you have deeply-nested layouts; sometimes it's handy to be able to override sizeHint(). In Python, a quick way to do this is with Lambda functions, like this:

textedit.sizeHint = lambda : qtc.QSize(500, 500)

Note that sizeHint() must return a QtCore.QSize object, not just an integer tuple.

A final way to control the size of widgets when using a box layout is to set a stretch factor when adding the widget to the layout. Stretch is an optional second parameter of addWidget() that defines the comparative stretch of each widget.

This example shows the use of the stretch factor:

stretch_layout = qtw.QHBoxLayout() layout.addLayout(stretch_layout)

stretch_layout.addWidget(qtw.QLineEdit('Short'), 1) stretch_layout.addWidget(qtw.QLineEdit('Long'), 2)

stretch only works with the QHBoxLayout and QVBoxLayout classes.

In this example, we've added a line edit with a stretch factor of 1, and a second with a stretch factor of 2. When you run this, you'll find that the second line edit is about twice the length of the first.

Keep in mind that stretch doesn't override the size hint or size policies, so depending on those factors the stretch ratios may not be exactly as specified.

Container widgets

We have seen that we can use QWidget as a container for other widgets. Qt also provides us with some special widgets that are specifically designed to contain other widgets. We'll look at two of these: QTabWidget and QGroupBox.

QTabWidget

QTabWidget, sometimes known as a notebook widget in other toolkits, allows us to have multiple pages selectable by tabs. They're very useful for breaking complex interfaces into smaller chunks that are easier for users to take in.

The workflow for using QTabWidget is as follows:

  1. Create the QTabWidget object
  2. Build a UI page on a QWidget or other widget class
  3. Add the page to the tab widget using the QTabWidget.addTab() method

Let's try that; first, create the tab widget:

tab_widget = qtw.QTabWidget() layout.addWidget(tab_widget)

Next, let's move the grid_layout we built under the Placing and arranging widgets section to a container widget:

container = qtw.QWidget(self)

grid_layout = qtw.QGridLayout()

  • comment out this line:

#layout.addLayout(grid_layout) container.setLayout(grid_layout)

Finally, let's add our container widget to a new tab:

tab_widget.addTab(container, 'Tab the first')

The second argument to addTab() is the title text that will appear on the tab. Subsequent tabs can be added with more calls to addTab(), like this:

tab_widget.addTab(subwidget, 'Tab the second')

The insertTab() method can also be used to add new tabs somewhere other than the end. QTabWidget has a few properties we can customize, listed here:

Property Arguments Description
movable Boolean Whether the tabs can be reordered. The default is False.
tabBarAutoHide Boolean Whether the tab bar is hidden or shown when there is only one tab.
tabPosition QTabWidget.TabPosition Which side of the widget the tabs appear on. The default is North (top).
tabShape QTabWidget.TabShape The shape of the tabs. It can be rounded or triangular.
tabsClosable Boolean Whether to display a close button on the tabs.
useScrollButtons Boolean Whether to use scroll buttons when there are many tabs or to expand.
Let's amend our QTabWidget to have movable, triangular tabs on the left side of the widget:

tab_widget = qtw.QTabWidget( movable=True, tabPosition=qtw.QTabWidget.West, tabShape=qtw.QTabWidget.Triangular )

QStackedWidget is similar to the tab widget, except that it contains no built-in mechanism for switching pages. You may find it useful if you want to build your own tab-switching mechanism.

QGroupBox

QGroupBox provides a panel that is labeled and (depending on the platform style) bordered. It's useful for grouping related input together on a form. We create the QGroupBox just as we would create a QWidget container, except that it can have a border and a title for the box, for example:

groupbox = qtw.QGroupBox('Buttons')

groupbox.setLayout(qtw.QHBoxLayout()) groupbox.layout().addWidget(qtw.QPushButton('OK')) groupbox.layout().addWidget(qtw.QPushButton('Cancel')) layout.addWidget(groupbox)

Here, we create a group box with the Buttons title. We gave it a horizontal layout and added two button widgets.

Notice in this example, instead of giving the layout a handle of its own as we've been doing, we create an anonymous QHBoxLayout and then use the widget's layout() accessor method to retrieve a reference to it for adding widgets. You may prefer this approach in certain situations.

The group box is fairly simple, but it does have a few interesting properties:

Property Argument Description
title string The title text.
checkable Boolean Whether the groupbox has a checkbox to enable/disable its contents.
checked Boolean Whether a checkable groupbox is checked (enabled).
alignment QtCore.Qt.Alignment The alignment of the title text.
flat Boolean Whether the box is flat or has a frame.
The checkable and checked properties are very useful for situations where you want a user to be able to disable entire sections of a form (for example, to disable the billing address part of an order form if it's the same as shipping address).

Let's reconfigure our groupbox, like so:

groupbox = qtw.QGroupBox( 'Buttons', checkable=True, checked=True, alignment=qtc.Qt.AlignHCenter, flat=True

)

Notice that now the buttons can be disabled with a simple checkbox toggle, and the frame has a different look.

If you just want a bordered widget without a label or checkbox capabilities, the QFrame class might be a better alternative.

Validating widgets

Although Qt provides a wide range of ready-made input widgets for things such as dates and numbers, we may find sometimes that we need a widget with very specific constraints on its input values. Such input constraints can be created using the QValidator class.

The workflow is like this:

  1. Create a custom validator class by subclassing QtGui.QValidator
  2. Override the validate() method with our validation logic
  3. Assign an instance of our custom class to a widget's validator property

Once assigned to an editable widget, the validate() method will be called every time the user updates the value of the widget (for example, every keystroke in QLineEdit) and will determine whether the input is accepted.

Creating an IPv4 entry widget

To demonstrate widget validation, let's create a widget that validates Internet Protocol version 4 (IPv4) addresses. An IPv4 address must be in the format of 4 integers, each between 0 and 255, with a dot between each number.

Let's start by creating our validator class. Add this class just before the MainWindow class:

class IPv4Validator(qtg.QValidator):

"""Enforce entry of IPv4 Addresses"""

Next, we need to override this class's validate() method. validate() receives two pieces of information: a string that contains the proposed input and the index at which the input occurred. It will have to return a value that indicates whether the input is Acceptable, Intermediate, or Invalid. If the input is acceptable or intermediate, it will

be accepted. If it's invalid, it will be rejected.

The value used to indicate the input state is either QtValidator.Acceptable, QtValidator.Intermediate, or QtValidator.Invalid.

In the Qt documentation, we're told that the validator class should only return the state constant. In PyQt, however, you actually need to return a tuple that contains the state, the string, and the position. This doesn't seem to be well-documented, unfortunately, and the error if you should forget this is not intuitive at all.

Let's start building our IPv4 validation logic as follows:

  1. Split the string on the dot character:

def validate(self, string, index): octets = string.split('.')

  1. If there are more than 4 segments, the value is invalid:

if len(octets) > 4:

state = qtg.QValidator.Invalid

  1. If any populated segment is not a digit string, the value is invalid:

elif not all([x.isdigit() for x in octets if x != '']): state = qtg.QValidator.Invalid

  1. If not every populated segment can be converted into an integer between 0 and 255, the value is invalid:

elif not all([0 <= int(x) <= 255 for x in octets if x != '']): state = qtg.QValidator.Invalid

  1. If we've made it this far into the checks, the value is either intermediate or valid. If there are fewer than four segments, it's intermediate:

elif len(octets) < 4:

state = qtg.QValidator.Intermediate

  1. If there are any empty segments, the value is intermediate:

elif any([x == '' for x in octets]):

state = qtg.QValidator.Intermediate

  1. If the value has passed all these tests, it's acceptable. We can return our tuple:

else:

state = qtg.QValidator.Acceptable return (state, string, index)

To use this validator, we just need to create an instance of it and assign it to a widget:

  • set the default text to a valid value

line_edit.setText('0.0.0.0') line_edit.setValidator(IPv4Validator())

If you run the demo now, you'll see that the line edit now constrains you to a valid IPv4 address.

Using QSpinBox for discrete values

As you learned earlier under the Creating basic QtWidgets widgets section, QSpinBox can be used for discrete lists of string values, much like a combobox. QSpinBox has a built-in validate() method that works just like the QValidator class' method to constrain input

to the widget. To make a spinbox use discrete string lists, we need to subclass QSpinBox and override validate() and two other methods, valueFromText() and

textFromValue().

Let's create a custom spinbox class that can be used to choose items from a list; just before the MainWindow class, enter this:

class ChoiceSpinBox(qtw.QSpinBox):

"""A spinbox for selecting choices."""

def __init__(self, choices, *args, **kwargs): self.choices = choices super().__init__(

*args,

maximum=len(self.choices) - 1, minimum=0,

**kwargs

)

We're subclassing qtw.QSpinBox and overriding the constructor so that we can pass in a

list or tuple of choices, storing it as self.choices. Then we call the QSpinBox constructor; note that we set the maximum and minimum so that they can't be set outside the bounds of our choices. We're also passing along any extra positional or keyword arguments so that we can take advantage of all the other QSpinBox property settings.

Next, let's reimplement valueFromText(), as follows:

def valueFromText(self, text):

return self.choices.index(text)

The purpose of this method is to be able to return an integer index value given a string that matches one of the displayed choices. We're simply returning the list index of whatever string is passed in.

Next, we need to reimplement the complimentary method, textFromValue():

def textFromValue(self, value): try:

return self.choices[value] except IndexError:

return '!Error!'

The purpose of this method is to translate an integer index value into the text of the matching choice. In this case, we're just returning the string at the given index. If somehow the widget gets passed a value out of range, we're returning !Error! as a string. Since this method is used to determine what is displayed in the box when a particular value is set, this would clearly show an error condition if somehow the value were out of range.

Finally, we need to take care of validate(). Just as we did with our QValidator class, we need to create a method that takes the proposed input and edit index and returns a tuple that contains the validation state, string value, and index.

We'll code it like this:

def validate(self, string, index):

if string in self.choices:

state = qtg.QValidator.Acceptable

elif any([v.startswith(string) for v in self.choices]): state = qtg.QValidator.Intermediate

else:

state = qtg.QValidator.Invalid

return (state, string, index)

In our method, we're returning Acceptable if the input string is found in self.choices, Intermediate if any choice starts with the input string (this includes a blank string), or Invalid in any other case.

With this class created, we can create one of our widgets in our MainWindow class:

ratingbox = ChoiceSpinBox(

['bad', 'average', 'good', 'awesome'], self

)

sublayout.addWidget(ratingbox)

An important difference between a QComboBox object and a

QSpinBox object with text options is that the spinbox items lack a data property. Only the text or index can be returned. It's best used for things such as months, days of the week, or other sequential lists that translate meaningfully into integer values.

Building a calendar application GUI

It's time to put what we've learned into action and actually build a simple, functional GUI. Our goal is to build a simple calendar application that looks like this:

Our interface won't be functional yet; for now, we'll just focus on getting the components created and laid out as shown in the screenshot. We'll do this two ways: once using code only, and a second time using Qt Designer.

Either of these methods is valid and work fine, though as you'll see, each has advantages and disadvantages.

Building the GUI in code

Create a new file called calendar_form.py by copying the application template from Chapter 1, Getting Started with PyQt.

Then we'll configure our main window; in the MainWindow constructor, begin with this code:

self.setWindowTitle("My Calendar App") self.resize(800, 600)

This code will set our window title to something appropriate and set a fixed size for our window of 800 x 600. Note that this is just the initial size, and the user will be able to resize the form if they wish to.

Creating the widgets

Now, let's create all of our widgets:

self.calendar = qtw.QCalendarWidget()

self.event_list = qtw.QListWidget()

self.event_title = qtw.QLineEdit()

self.event_category = qtw.QComboBox()

self.event_time = qtw.QTimeEdit(qtc.QTime(8, 0)) self.allday_check = qtw.QCheckBox('All Day')

self.event_detail = qtw.QTextEdit()

self.add_button = qtw.QPushButton('Add/Update') self.del_button = qtw.QPushButton('Delete')

These are all of the widgets we will be using in our GUI. Most of these we have covered already, but there are two new ones: QCalendarWidget and QListWidget.

QCalendarWidget is exactly what you'd expect it to be: a fully interactive calendar that can be used to view and select dates. Although it has a number of properties that can be configured, for our needs the default configuration is fine. We'll be using it to allow the user to select the date to be viewed and edited.

QListWidget is for displaying, selecting, and editing items in a list. We're going to use it to show a list of events saved on a particular day.

Before we move on, we need to configure our event_category combo box with some items to select. Here's the plan for this box:

  • Have it read Select category… as a placeholder when nothing is selected
  • Include an option called New… which might perhaps allow the user to enter a new category
  • Include some common categories by default, such as Work, Meeting, and Doctor

To do this, add the following:

  • Add event categories

self.event_category.addItems( ['Select category…', 'New…', 'Work',

'Meeting', 'Doctor', 'Family']

)

  • disable the first category item

self.event_category.model().item(0).setEnabled(False)

QComboBox doesn't really have placeholder text, so we're using a trick here to simulate it. We've added our combo box items using the addItems() method as usual. Next, we retrieve its data model using the model() method, which returns a QStandardItemModel instance. The data model holds a list of all the items in the combo box. We can use the model's item() method to access the actual data item at a given index (in this case 0) and use its setEnabled() method to disable it.

In short, we've simulated placeholder text by disabling the first entry in the combo box.

We'll learn more about widget data models in Chapter 5, Creating Data Interfaces with Model-View Classes.

Building the layout

Our form is going to require some nested layouts to get everything into position. Let's break down our proposed design and determine how to create this layout:

  • The application is divided into a calendar on the left and a form on the right. This suggests using QHBoxLayout for the main layout.
  • The form on the right is a vertical stack of components, suggesting we use QVBoxLayout to arrange things on the right.
  • The event form at the bottom right can be laid out roughly in a grid so we could use QGridLayout there.

We'll begin by creating the main layout and adding in the calendar:

main_layout = qtw.QHBoxLayout() self.setLayout(main_layout) main_layout.addWidget(self.calendar)

We want the calendar widget to fill any extra space in the layout, so we'll set its size policy accordingly:

self.calendar.setSizePolicy( qtw.QSizePolicy.Expanding, qtw.QSizePolicy.Expanding )

Now, let's create the vertical layout on the right, and add the label and event list:

right_layout = qtw.QVBoxLayout() main_layout.addLayout(right_layout)

right_layout.addWidget(qtw.QLabel('Events on Date')) right_layout.addWidget(self.event_list)

In the event that there's more vertical space, we'd like the event list to fill all the available space. So, let's set its size policy as follows:

self.event_list.setSizePolicy( qtw.QSizePolicy.Expanding, qtw.QSizePolicy.Expanding )

The next part of our GUI is the event form and its label. We could use another label here, but the design suggests that these form fields are grouped together under this heading so QGroupBox would be more appropriate.

So, let's create a group box with QGridLayout to hold our event form:

event_form = qtw.QGroupBox('Event') right_layout.addWidget(event_form)

event_form_layout = qtw.QGridLayout() event_form.setLayout(event_form_layout)

Finally, we need to add in our remaining widgets into the grid layout:

event_form_layout.addWidget(self.event_title, 1, 1, 1, 3) event_form_layout.addWidget(self.event_category, 2, 1)

event_form_layout.addWidget(self.event_time, 2, 2,)

event_form_layout.addWidget(self.allday_check, 2, 3)

event_form_layout.addWidget(self.event_detail, 3, 1, 1, 3) event_form_layout.addWidget(self.add_button, 4, 2)

event_form_layout.addWidget(self.del_button, 4, 3)

We're dividing our grid into three columns, and using the optional column-span argument to put our title and detail fields across all three columns.

And now we're done! At this point, you can run the script and see your completed form. It doesn't do anything yet, of course, but that is a topic for our Chapter 3, Handling Events

with Signals and Slots.

Building the GUI in Qt Designer

Let's try building the same GUI, but this time we'll build it using Qt Designer. First steps

To begin, launch Qt Designer as described in Chapter 1, Getting Started with PyQt, then create a new form based on a widget, like this:

Now, click on the Widget and we'll configure its properties using the Properties panel on the right:

  1. Change the object name to MainWindow
  2. Under Geometry, change the Width to 800 and Height to 600
  3. Change the window title to My Calendar App

Next, we'll start adding in the widgets. Scroll through the widget box on the left to find the Calendar Widget, then drag it onto the main window. Select the calendar and edit its properties:

  1. Change the name to calendar
  2. Change the horizontal and vertical size policies to Expanding

To set up our main layout, right-click the main window (not on the calendar) and select Layout | Lay Out Horizontally. This will add a QHBoxLayout to the main window widget. Note that you can't do this until at least one widget is on the main window, which is why we added the calendar widget first.

Building the right panel

Now, we'll add the vertical layout for the right side of the form. Drag a Vertical Layout to the right of the calendar widget. Then drag a Label Widget into the vertical layout. Make sure the label is listed hierarchically as a child of the vertical layout, not a sibling:

If you are having trouble dragging the widget onto the unexpanded layout, you can also drag it into the hierarchy in the Object Inspector panel.

Double-click the text on the label and change it to say Events on Date.

Next, drag a List Widget onto the vertical layout so that it appears under the label. Rename it event_list and check its properties to make sure its size policies are set to Expanding.

Building the event form

Find the Group Box in the widget box and drag it under the list widget. Double-click the text and change it to Event.

Drag a Line Edit onto the group box, making sure it shows up as a child of the group box in the Object Inspector. Change the object name to event_title.

Now, right-click the group box and select Lay out, then select Lay out in a Grid. This will create a grid layout in the group box.

Drag a Combo Box onto the next line. Drag a Time Edit to the right of it, then a Check Box to the right of that. Name them event_category, event_time, and allday_check, respectively. Double-click the checkbox text and change it to All Day.

To add options to the combo box, right-click the box and select Edit Items. This will open a dialog where we can type in our items, so click the + button to add Select

Category… like the first one, then New…, then a few random categories (such as Work, Doctor, Meeting).

Unfortunately, we can't disable the first item using Qt Designer. We'll have to handle that when we use our form in an application, which we'll discuss in Chapter 3, Handling Events with Signals and Slots.

Notice that adding those three widgets pushed the line edit over to the right. We need to fix the column span on that widget. Click the line edit, grab the handle on the right edge, and drag it right until it expands to the width of the group box.

Now, grab a Text Edit and drag it under the other widgets. Notice that it's squashed into the first column, so just as with the line edit, drag it right until it fills the whole width. Rename the text edit to event_detail.

Finally, drag two Push Button widgets to the bottom of the form. Make sure to drag them to the second and third columns, leaving the first column empty. Rename

them add_button and del_button, changing the text to Add/Update and Delete, respectively.

Previewing the form

Save the form as calendar_form.ui, then press Ctrl + R to preview it. You should see a

fully functional form, just as shown in the original screenshot. To actually use this file, we'll have to transpile it to Python code and import it into an actual script. We'll cover this

in Chapter 3, Handling Events with Signals and Slots, after we've made some additional modifications to the form.

Summary

In this chapter, we covered a selection of the most popular widget classes in Qt. You learned how to create them, customize them, and add them to a form. We discussed various ways to control widget sizes and practiced building a simple application form in both Python code and the Qt Designer WYSIWYG application.

In the next chapter, we'll learn how to make this form actually do something as we explore Qt's core communication and event-handling system. Keep your calendar form handy, as we'll modify it some more and make a functional application from it.

Questions

Try these questions to test your knowledge from this chapter:

  1. How would you create a QWidget that is fullscreen, has no window frame, and uses the hourglass cursor?
  2. You're asked to design a data-entry form for a computer inventory database. Choose the best widget to use for each of the following fields:
  • Computer make: One of eight brands that your company purchases
  • Processor Speed: The CPU speed in GHz
  • Memory amount: The amount of RAM, in whole MB
  • Host Name: The computer's hostname
  • Video make: Whether the video hardware is Nvidia, AMD, or Intel
  • OEM License: Whether the computer uses an Original Equipment Manufacturer (OEM) license
  1. The data entry form includes an inventory number field that requires the XX-999-9999X format where X is an uppercase letter from A to Z, excluding O and I, and 9 is a number from 0 to 9. Can you create a validator class to validate this input?
  2. Check out the following calculator form—what layouts may have been used to create it?

  1. Referring to the preceding calculator form, how would you make the button grid take up any extra space when the form is resized?
  2. The topmost widget in the calculator form is a QLCDNumber widget. Can you find the Qt documentation on this widget? What unique properties does it have? When might you use it?
  3. Starting with your template code, build the calculator form in code.
  4. Build the calculator form in Qt Designer.

Further reading

Check out the following resources for more information on the topics covered in this chapter:

  • The QWidget properties documentation lists all the properties for QWidget,

which are inherited by all its child classes, at https://doc.qt.io/qt-5/qwidget. html#properties

[ 59 ] )

'Mastering-GUI-Programming' 카테고리의 다른 글

Styling Qt Applications  (0) 2023.03.08
Building Applications with QMainWindow  (0) 2023.03.08
Handling Events with Signals and Slots  (0) 2023.03.08
Deep Dive into PyQt  (0) 2023.03.08

댓글