12. Building Complex UIs with QMainWindow

QMainWindow is a QWidget subclass with built-in support for toolbars, dock widgets, menu bars, and status bars. It is useful as a starting point for building applications that have a central area and those elements.

[TODO: MAIN WINDOW LAYOUT IMAGE]

It handles layout and docking automatically, saving you from manual QWidget management.

12.1 Setting Up the Central Widget

A central widget ca be any of the standard widgets, typically a QTextEdit, a QTableView, or a QGraphicsView. It is set with setCentralWidget().

An icon of a clipboard-list1

You are building a simple note-taking application. It needs to support rich text editing (bold, italic, different font sizes, etc.), and you anticipate more features in the future, such as menus, toolbars, and side panels. You decide to use QMainWindow as the main window and place a QTextEdit as its central widget.

To use QMainWindow in your application:

 1 import sys
 2 from PySide6.QtWidgets import (QApplication,
 3     QMainWindow, QTextEdit)
 4 
 5 class Editor(QMainWindow):
 6     
 7     def __init__(self, parent=None):
 8 
 9         super().__init__(parent)
10         self.setWindowTitle('Acme Editor')
11         self.resize(500, 300)
12         text_edit = QTextEdit()        
13         self.setCentralWidget(text_edit)
14 
15 if __name__ == '__main__':
16 
17     app = QApplication(sys.argv)
18     editor = Editor()
19     editor.show()
20     sys.exit(app.exec())
  1. Create a class that inherits from QMainWindow and name it Editor.

  2. Inside the Editor class’s __init__() method, instantiate a QTextEdit widget. This will serve as the primary area for viewing and editing notes.

  3. Call self.setCentralWidget() on the QMainWindow instance, passing the QTextEdit object. This places the text editor in the window’s central area, automatically handling resizing and layout.

When you run the application, you see a window filled with the editable text area. You can type, select, copy, and paste text. The context menu and both scrollbars are present but text formatting (bold, italic, font size) is not available.

12.2 Adding a Status Bar

The QStatusBar class is a horizontal bar at the bottom of a QMainWindow, used for displaying status information. It supports three types of messages:

  • Temporary - briefly shown over normal messages but not permanent ones. Used for notifications, tool tip explanations, or menu help text.

  • Normal - displayed persistently on the left side. Used for dynamic information like cursor position, document length, or similar.

  • Permanent - always visible on the right side. Used for indicators such as Caps Lock status or character encoding.

An icon of a clipboard-list1

Your note-taking application users have requested live feedback at the bottom of the window: the current cursor line and column, the total character count, and (when text is selected) a note showing how many characters are selected.

To add a status bar to the main window:

 1 #  The QStatusBar class provides a horizontal bar 
 2 # suitable for presenting status information. 
 3 
 4 import sys
 5 from PySide6.QtCore import Slot
 6 from PySide6.QtWidgets import (QApplication,
 7     QMainWindow, QTextEdit, QLabel)
 8 
 9 class Editor(QMainWindow):
10     
11     def __init__(self, parent=None):
12 
13         super().__init__(parent)
14         self.setWindowTitle('Acme Editor')
15         self.resize(500, 300)
16         self.text_edit = QTextEdit()        
17         self.setCentralWidget(self.text_edit)
18         self.position_label = QLabel()
19         self.charcount_label = QLabel()
20         self.statusBar().addWidget(self.position_label)
21         self.statusBar().addPermanentWidget(self.charcount_label)
22         
23         self.text_edit.textChanged.connect(self.update_stats)
24         self.text_edit.selectionChanged.connect(
25             self.show_selection_size)
26     
27     @Slot()
28     def update_stats(self):
29         cursor = self.text_edit.textCursor()
30         size = self.text_edit.document().characterCount()
31         x = str(cursor.blockNumber() + 1)
32         y = str(cursor.columnNumber() + 1)
33         self.position_label.setText(f'Ln: {x}, Col: {y}')
34         self.charcount_label.setText(f'Chars: {size}')        
35     
36     @Slot()
37     def show_selection_size(self):
38         cursor = self.text_edit.textCursor()
39         count = len(cursor.selectedText())
40         msg = f'{count} characters selected'
41         self.statusBar().showMessage(msg, 2000)
42 
43 
44 if __name__ == '__main__':
45 
46     app = QApplication(sys.argv)
47     editor = Editor()
48     editor.show()
49     sys.exit(app.exec())
  1. Create QLabel widgets for the information you want to display persistently. In our example, we use one label for cursor position and another for total character count.

  2. Access the status bar with self.statusBar() and add the labels using addWidget() for normal messages and addPermanentWidget() for permanent ones.

  3. Connect QTextEdit signals to update the information:

    • textChanged() to refresh cursor position and character count.
    • selectionChanged() to show the selected character count temporarily.

In the example, the permanent label always shows the total character count, while the normal label shows the cursor position. When text is selected, we briefly display a temporary message showing the selection size (e.g., “42 characters selected”). The temporary message hides the normal label for a couple of seconds but leaves the permanent character count visible.

12.3 Creating Menus and Actions

Menus are constructed using QMenuBar as the top-level container, with QMenu objects for each dropdown group, and QAction objects for the individual commands or items. QActions can be reused across menus, toolbars, and keyboard shortcuts.

[TODO: MENUBAR DIAGRAM]

An icon of a clipboard-list1

Your application users expect standard application menus: a File menu with an Exit command and a Help menu with an About dialog that shows application information and version.

To add drop-down menus to your main window:

 1 # The QMenu class provides a menu widget for use 
 2 # in menu bars, context menus, and other popup menus. 
 3 
 4 # The QAction class provides an abstraction for user commands
 5 # Same action objects can be added to 
 6 # menus, toolbars and keyboard shortcuts.
 7 
 8 import sys
 9 from PySide6.QtCore import Slot
10 from PySide6.QtGui import QAction
11 from PySide6.QtWidgets import (QApplication, QMainWindow,
12     QTextEdit, QLabel, QMessageBox)
13 
14 
15 class Editor(QMainWindow):
16     
17     def __init__(self, parent=None):
18 
19         super().__init__(parent)
20         self.setWindowTitle('Acme Editor')
21         self.resize(500, 300)
22 
23         self.text_edit = QTextEdit()        
24         self.setCentralWidget(self.text_edit)
25 
26         self.position_label = QLabel()
27         self.charcount_label = QLabel()
28         self.statusBar().addWidget(self.position_label)
29         self.statusBar().addPermanentWidget(self.charcount_label)
30         
31         self.text_edit.textChanged.connect(self.update_stats)
32         self.text_edit.selectionChanged.connect(
33             self.show_selection_size)
34         
35         # You can access the main window QMenuBar
36         # using QMainWindow.menuBar()
37         
38         menu_bar = self.menuBar()
39         
40         # 1 - Create a QMenu instance using QMenuBar.addMenu() 
41         #     Use the ampersand to make keyboard shortcuts work.
42         
43         file_menu = menu_bar.addMenu('&File')
44         
45         # 2 - Create a QAction instance.
46         #     Connect a slot to its triggered signal.
47         #     Set Editor as QAction's parent.
48         
49         exit_action = QAction(self)
50         exit_action.setText('Exit')
51         exit_action.setShortcut('Alt+X')
52         exit_action.triggered.connect(QApplication.quit)
53         
54         # 3 - Add action to the menu.
55         
56         file_menu.addAction(exit_action)
57         
58         # Repeat the steps for each menu item
59         
60         help_menu = menu_bar.addMenu('&Help')
61         
62         about_action = QAction(self)
63         about_action.setText('About')
64         about_action.triggered.connect(self.show_messagebox)
65         
66         help_menu.addAction(about_action)
67     
68     @Slot()
69     def update_stats(self):
70         cursor = self.text_edit.textCursor()
71         size = self.text_edit.document().characterCount()
72         x = str(cursor.blockNumber() + 1)
73         y = str(cursor.columnNumber() + 1)
74         self.position_label.setText(f'Ln: {x}, Col: {y}')
75         self.charcount_label.setText(f'Chars: {size}')
76         
77     @Slot()
78     def show_selection_size(self):
79         cursor = self.text_edit.textCursor()
80         count = len(cursor.selectedText())
81         msg = f'{count} characters selected'
82         self.statusBar().showMessage(msg, 2000)
83     
84     @Slot()
85     def show_messagebox(self):
86         messagebox = QMessageBox()
87         messagebox.setText('QMainWindow Example\nVersion 1.0')
88         messagebox.exec()
89 
90 
91 if __name__ == '__main__':
92 
93     app = QApplication(sys.argv)
94     editor = Editor()
95     editor.show()
96     sys.exit(app.exec())
  1. Access the main window’s menu bar with QMainWindow.menuBar(). Add individual menus using QMenuBar.addMenu() with a title (e.g., ‘&File’). Ampersands enable keyboard shortcuts.

  2. For each menu item, create a QAction object. Set its text, a shortcut with setShortcut(), and connect its triggered() signal to a slot (e.g., for quitting or showing a dialog). Set the main window as the action parent to ensure it stays in scope for the application lifetime.

  3. Add the action to the menu using QMenu.addAction().

Menu-level keyboard accelerators are enabled by prefixing letters with an ampersand (&) in the menu title (e.g., “&File” allows Alt+F to open it). For action-level shortcuts (e.g., Ctrl+Q for Exit), use setShortcut().

When you run the application, the menu bar appears at the top. Selecting “Exit” closes the app, and “About” displays a simple dialog.

12.4 Adding Toolbars

QToolBar provides a panel for quick-access controls. Users can drag a toolbar to different dock areas, float them as separate windows, or customize their position. You can reuse the same QAction objects for both menus and toolbars.

An icon of a clipboard-list1

You are enhancing the note-taking application with a toolbar. You decide to place the Exit and About commands on it.

To use a toolbar in your application:

 1 # The QToolBar class provides a movable 
 2 # panel that contains a set of controls.
 3 
 4 import sys
 5 from PySide6.QtCore import Qt, Slot
 6 from PySide6.QtGui import QAction, QIcon
 7 from PySide6.QtWidgets import (QApplication, QMainWindow,
 8     QTextEdit, QLabel, QMessageBox)
 9 
10 
11 class QEditor(QMainWindow):
12     
13     def __init__(self, parent=None):
14 
15         super().__init__(parent)
16         self.setWindowTitle('Acme Editor')
17         self.resize(500, 300)
18 
19         self.text_edit = QTextEdit()        
20         self.setCentralWidget(self.text_edit)
21 
22         self.position_label = QLabel()
23         self.charcount_label = QLabel()
24         self.statusBar().addWidget(self.position_label)
25         self.statusBar().addPermanentWidget(self.charcount_label)
26         
27         self.text_edit.textChanged.connect(self.update_stats)
28         self.text_edit.selectionChanged.connect(
29             self.show_selection_size)
30         
31         menu_bar = self.menuBar()
32         file_menu = menu_bar.addMenu('&File')
33         
34         # You can add icons to QActions.
35         # Icons are shown both in the menu and in the toolbar.
36         # The icons used here are from the Tango project.
37         
38         exit_action = QAction(self)
39         exit_action.setText('Exit')
40         exit_action.setShortcut('Alt+X')
41         exit_action.setIcon(QIcon('./icons/exit.png'))
42         exit_action.triggered.connect(QApplication.quit)
43         
44         file_menu.addAction(exit_action)
45           
46         help_menu = menu_bar.addMenu('&Help')
47         
48         about_action = QAction(self)
49         about_action.setText('About')
50         about_action.setShortcut('Alt+A')
51         about_action.setIcon(QIcon('./icons/about.png'))
52         about_action.triggered.connect(self.show_messagebox)
53         
54         help_menu.addAction(about_action)
55         
56         # 1 - Create the toolbar
57         
58         file_toolbar = self.addToolBar('File')
59         
60         # 2 - Add actions to it. We reuse the same actions
61         #     that we used for the menu.
62         
63         file_toolbar.addAction(exit_action)
64         file_toolbar.addAction(about_action)
65         
66         # 3 - ... and that's about it. Here we just
67         #     set the icons to be displayed besides the text.
68         
69         file_toolbar.setToolButtonStyle(
70             Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
71     
72     @Slot()
73     def update_stats(self):
74         cursor = self.text_edit.textCursor()
75         size = self.text_edit.document().characterCount()
76         x = str(cursor.blockNumber() + 1)
77         y = str(cursor.columnNumber() + 1)
78         self.position_label.setText(f'Ln: {x}, Col: {y}')
79         self.charcount_label.setText(f'Chars: {size}')
80         
81     @Slot()
82     def show_selection_size(self):
83         cursor = self.text_edit.textCursor()
84         count = len(cursor.selectedText())
85         msg = f'{count} characters selected'
86         self.statusBar().showMessage(msg, 2000)
87         
88     def show_messagebox(self):
89         messagebox = QMessageBox()
90         messagebox.setText('QMainWindow Example\nVersion 1.1')
91         messagebox.exec()
92 
93 
94 if __name__ == '__main__':
95 
96     app = QApplication(sys.argv)
97     editor = QEditor()
98     editor.show()
99     sys.exit(app.exec())
  1. Create the toolbar using QMainWindow.addToolBar().

  2. Add existing QAction objects (reused from menus), custom QWidgets, or separators as needed.

  3. Optionally, add icons to the actions with setIcon(). Icons display in both menus and toolbars. Customize the toolbar’s appearance with setToolButtonStyle().

When you run the application, the toolbar appears at the top. Hovering shows tooltips (inherited from action text), and clicking performs the same functions as the menu. Users can right-click the toolbar area to toggle visibility or drag it to reposition.

12.5 Using Dock Widgets

QDockWidget enables the creation of panels that can be docked within a QMainWindow or floated as independent windows. It is used for tools that users may want to rearrange or hide to customise their workflow.

Your text editor users want quick access to common formatting tools (bold, italic, and font size selection) without cluttering the main menu or toolbar. You provide these controls in a dockable widget that can be positioned on the left or right and floated if needed.

To use a dock widget in your application:

  1 # The QDockWidget class provides a widget that can be 
  2 # docked inside a QMainWindow or floated 
  3 # as a top-level window on the desktop
  4 
  5 import sys
  6 from PySide6.QtCore import Qt, Slot
  7 from PySide6.QtGui import QAction, QIcon,QTextCharFormat, QFont
  8 from PySide6.QtWidgets import (QApplication, QMainWindow,
  9     QTextEdit, QLabel, QMessageBox, QVBoxLayout, QPushButton,
 10     QSpinBox, QDockWidget, QWidget)
 11 
 12 
 13 class Editor(QMainWindow):
 14     
 15     def __init__(self, parent=None):
 16 
 17         super().__init__(parent)
 18         self.setWindowTitle('Acme Editor')
 19         self.resize(500, 300)
 20 
 21         self.text_edit = QTextEdit()
 22         self.text_edit.cursorPositionChanged.connect(
 23             self.update_dock_widgets)
 24         self.setCentralWidget(self.text_edit)
 25 
 26         self.position_label = QLabel()
 27         self.charcount_label = QLabel()
 28         self.statusBar().addWidget(self.position_label)
 29         self.statusBar().addPermanentWidget(self.charcount_label)
 30         
 31         self.text_edit.textChanged.connect(self.update_stats)
 32         self.text_edit.selectionChanged.connect(
 33             self.show_selection_size)
 34         
 35         menu_bar = self.menuBar()
 36         file_menu = menu_bar.addMenu('&File')
 37         
 38         exit_action = QAction(self)
 39         exit_action.setText('Exit')
 40         exit_action.setShortcut('Alt+X')
 41         exit_action.setIcon(QIcon('./icons/exit.png'))
 42         exit_action.triggered.connect(QApplication.quit)
 43         
 44         file_menu.addAction(exit_action)
 45           
 46         help_menu = menu_bar.addMenu('&Help')
 47         
 48         about_action = QAction(self)
 49         about_action.setText('About')
 50         about_action.setShortcut('Alt+A')
 51         about_action.setIcon(QIcon('./icons/about.png'))
 52         about_action.triggered.connect(self.show_messagebox)
 53         
 54         help_menu.addAction(about_action)
 55         
 56         file_toolbar = self.addToolBar('File')
 57         file_toolbar.addAction(exit_action)
 58         file_toolbar.addAction(about_action)
 59         file_toolbar.setToolButtonStyle(
 60             Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
 61         
 62         # 1. Create the dock widget
 63             
 64         dock_widget = QDockWidget('Formatting')
 65         dock_widget.setAllowedAreas(
 66             Qt.DockWidgetArea.LeftDockWidgetArea
 67             | Qt.DockWidgetArea.RightDockWidgetArea)
 68             
 69         vbox = QVBoxLayout()
 70         
 71         self.button_bold = QPushButton()
 72         self.button_bold.setIcon(QIcon('./icons/bold.png'))
 73         self.button_bold.setCheckable(True)
 74         self.button_bold.toggled.connect(self.update_bold)
 75         
 76         self.button_italic = QPushButton()
 77         self.button_italic.setIcon(QIcon('./icons/italic.png'))
 78         self.button_italic.setCheckable(True)
 79         self.button_italic.toggled.connect(self.update_italic)
 80         
 81         self.font_size_spinbox = QSpinBox()
 82         self.font_size_spinbox.setMinimumWidth(26)
 83         self.font_size_spinbox.setMinimum(1)
 84         self.font_size_spinbox.setMaximum(24)
 85         self.font_size_spinbox.valueChanged.connect(
 86             self.update_font_size)
 87         
 88         self.point_size = 12
 89         
 90         char_format = QTextCharFormat()
 91         char_format.setFontPointSize(self.point_size)
 92         self.text_edit.mergeCurrentCharFormat(char_format)
 93         self.font_size_spinbox.setValue(self.point_size)
 94         
 95         vbox.addWidget(self.button_bold)
 96         vbox.addWidget(self.button_italic)
 97         vbox.addWidget(self.font_size_spinbox)
 98         vbox.addStretch()
 99         
100         container = QWidget()
101         container.setLayout(vbox)
102         container.setMinimumWidth(20)
103         dock_widget.setWidget(container)
104         
105         # 2. Add the dock widget to the main window
106         
107         self.addDockWidget(
108             Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget)
109     
110     @Slot()
111     def update_stats(self):
112         cursor = self.text_edit.textCursor()
113         size = self.text_edit.document().characterCount()
114         x = str(cursor.blockNumber() + 1)
115         y = str(cursor.columnNumber() + 1)
116         self.position_label.setText(f'Ln: {x}, Col: {y}')
117         self.charcount_label.setText(f'Chars: {size}')
118     
119     @Slot()
120     def show_selection_size(self):
121         cursor = self.text_edit.textCursor()
122         count = len(cursor.selectedText())
123         msg = f'{count} characters selected'
124         self.statusBar().showMessage(msg, 2000)
125     
126     @Slot()
127     def show_messagebox(self):
128         messagebox = QMessageBox()
129         messagebox.setText('QMainWindow Example\nVersion 1.2')
130         messagebox.exec()
131     
132     # 3. Handle the dock widget children signals
133     
134     def update_bold(self, checked):
135         char_format = QTextCharFormat()
136         if checked:
137             char_format.setFontWeight(QFont.Weight.Bold)
138         else:
139             char_format.setFontWeight(QFont.Weight.Normal)
140         self.text_edit.mergeCurrentCharFormat(char_format)
141         self.text_edit.setFocus(Qt.FocusReason.OtherFocusReason)
142         
143     def update_italic(self, checked):
144         char_format = QTextCharFormat()
145         char_format.setFontItalic(checked)
146         self.text_edit.mergeCurrentCharFormat(char_format)
147         self.text_edit.setFocus(Qt.FocusReason.OtherFocusReason)
148         
149     def update_font_size(self, i):
150         char_format = QTextCharFormat()
151         char_format.setFontPointSize(i)
152         self.text_edit.mergeCurrentCharFormat(char_format)
153         
154     def update_dock_widgets(self):
155         char_format = self.text_edit.textCursor().charFormat() 
156         self.button_bold.setChecked(char_format.font().bold())
157         self.button_italic.setChecked(char_format.font().italic())
158         self.font_size_spinbox.setValue(char_format.font().pointSize())
159 
160 
161 if __name__ == '__main__':
162 
163     app = QApplication(sys.argv)
164     editor = Editor()
165     editor.show()
166     sys.exit(app.exec())
  1. Instantiate a QDockWidget and configure its properties, such as title or allowed docking areas.

  2. Create child widgets and arrange them in a layout within a container QWidget, then set this container as the dock’s content using setWidget().

  3. Connect signals from the child widgets to slots that apply formatting via QTextCharFormat and mergeCurrentCharFormat(). To reflect the current text format in the dock (e.g., when moving the cursor), connect QTextEdit.cursorPositionChanged() to update the widgets’ states.

12.6 Completing the Editor