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().
![]() |
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 |
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())
Create a class that inherits from
QMainWindowand name itEditor.Inside the
Editorclass’s__init__()method, instantiate aQTextEditwidget. This will serve as the primary area for viewing and editing notes.Call
self.setCentralWidget()on theQMainWindowinstance, passing theQTextEditobject. 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.
![]() |
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())
Create
QLabelwidgets for the information you want to display persistently. In our example, we use one label for cursor position and another for total character count.Access the status bar with
self.statusBar()and add the labels usingaddWidget()for normal messages andaddPermanentWidget()for permanent ones.-
Connect
QTextEditsignals 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]
![]() |
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())
Access the main window’s menu bar with
QMainWindow.menuBar(). Add individual menus usingQMenuBar.addMenu()with a title (e.g., ‘&File’). Ampersands enable keyboard shortcuts.For each menu item, create a
QActionobject. Set its text, a shortcut withsetShortcut(), and connect itstriggered()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.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.
![]() |
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())
Create the toolbar using
QMainWindow.addToolBar().Add existing
QActionobjects (reused from menus), customQWidgets, or separators as needed.Optionally, add icons to the actions with
setIcon(). Icons display in both menus and toolbars. Customize the toolbar’s appearance withsetToolButtonStyle().
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())
Instantiate a
QDockWidgetand configure its properties, such as title or allowed docking areas.Create child widgets and arrange them in a layout within a container
QWidget, then set this container as the dock’s content usingsetWidget().Connect signals from the child widgets to slots that apply formatting via
QTextCharFormatandmergeCurrentCharFormat(). To reflect the current text format in the dock (e.g., when moving the cursor), connectQTextEdit.cursorPositionChanged()to update the widgets’ states.
