I am making a header in my app with this format:
||Icon| Page QueTueDue v0.6-b3 |
- Icon (
self.header_menu) - Page name (
self.header_page_label) - App name (
self.header_label) - App version (
self.header_sub_label)
My problem is that the app name and version are offset from the center of the window (not the header layout). I could solve this with a QSpacerItem/QWidget with a certain width on one end, however the page name varies, and so does its width. How can I center the app name and version without the other widgets in the layout affecting it?
Code snippets:
Header definition and code in MainWindow:
self.header_layout = QHBoxLayout()
self.header_layout.setSpacing(8)
self.header_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.header_menu_layout = QHBoxLayout()
self.header_title_layout = QHBoxLayout()
self.header_menu = QMenu()
self.header_menu_file = QAction("File")
self.header_menu_file.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_file)
self.header_menu_add = QAction("Add")
self.header_menu_add.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_add)
self.header_menu_remove = QAction("Remove")
self.header_menu_remove.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_remove)
self.header_menu_mark_off = QAction("Mark Off")
self.header_menu_mark_off.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_mark_off)
self.header_menu_edit = QAction("Edit")
self.header_menu_edit.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_edit)
self.header_menu_button = QPushButton()
self.header_menu_button.setIcon(QIcon(os.path.join(ICON_PATH, f"header_menu_icon_{THEME}.png")))
self.header_menu_button.setIconSize(QSize(24, 24))
self.header_menu_button.setFixedSize(48, 32)
self.header_menu_button.setFlat(True)
self.header_menu_button.setMenu(self.header_menu)
self.header_page_label = QLabel("Tasks")
self.header_page_label.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
self.header_page_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
self.header_page_label.setFont(QFont(self.families[4][0]))
self.header_menu_layout.addWidget(self.header_menu_button)
self.header_menu_layout.addWidget(self.header_page_label)
self.header_menu_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.header_label = QLabel("QueTueDue")
self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.header_label.setContentsMargins(0, 4, 0, 4)
self.header_label.setFont(QFont(self.families[4], 12))
self.header_sub_label = QLabel(f"{__version__}")
self.header_sub_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self.header_sub_label_palette = self.header_sub_label.palette()
self.header_sub_label_palette.setColor(QPalette.ColorRole.WindowText, QColor(50, 50, 50))
self.header_sub_label.setPalette(self.header_sub_label_palette)
self.header_sub_label.setFont(QFont(self.families[4], 10))
self.header_title_spacer = QSpacerItem(
self.header_menu_button.width() + self.header_page_label.width() + 4,
10,
QSizePolicy.Policy.Minimum,
QSizePolicy.Policy.Minimum,
)
self.header_title_spacer.setAlignment(Qt.AlignmentFlag.AlignRight)
self.header_title_layout.addWidget(self.header_label)
self.header_title_layout.addWidget(self.header_sub_label)
self.header_title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
self.header_layout.addLayout(self.header_menu_layout)
self.header_layout.addLayout(self.header_title_layout)
self.header_layout.addItem(self.header_title_spacer)
self.header_separator = separator("h")
self.window_layout.addLayout(self.header_layout)
self.window_layout.addWidget(self.header_separator)
The function update_header_spacer to recalculate the width of the spacer:
def update_header_spacer(self):
"""Calculate and resize the width on the header spacer
(header_title_spacer)
"""
print(self.header_menu_button.width())
print(self.header_page_label.width())
print(self.header_menu_button.width() + self.header_page_label.width() + 4)
width = self.header_menu_button.width() + self.header_page_label.width() + 4
self.header_title_spacer.changeSize(
width,
1,
QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Minimum,
)
self.header_layout.invalidate()
And the change_page function to change the current widget on the main stack layout which makes up the main window layout (minus the header and toolbar):
def change_page(self, page, header_label, item=False):
"""Changes the current widget on the stack layout
(self.stack_layout).
"""
if self.stack_layout.currentWidget() == page:
self.stack_layout.setCurrentWidget(self.main_page)
self.header_page_label.setText("Tasks")
return
self.stack_layout.setCurrentWidget(page)
self.header_page_label.setText(header_label)
self.update_header_spacer()
Finally, MainWindow:
class MainWindow(QMainWindow):
"""The main app window containing all the categories, tasks toolbars
and more.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("QueTueDue")
self.setWindowIcon(QIcon(os.path.join(ICON_PATH, "logo.svg")))
self.setMinimumWidth(800)
# Define fonts
self.fonts = [
"AdwaitaMono-Regular.ttf",
"AdwaitaMono-Bold.ttf",
"AdwaitaMono-Italic.ttf",
"AdwaitaMono-BoldItalic.ttf",
"AdwaitaSans-Regular.ttf",
"AdwaitaSans-Italic.ttf",
]
self.font_dialogs = []
self.families = []
for self.font in self.fonts:
self.fontfile = os.path.join(FONT_PATH, self.font)
if not os.path.exists(self.fontfile):
process.startDetached("python", [os.path.join(ROOT_PATH, "file_checker.py")])
sys.exit()
else:
id = QFontDatabase.addApplicationFont(self.fontfile)
self.families.append(QFontDatabase.applicationFontFamilies(id))
# System tray icon
self.icon = QIcon(os.path.join(ICON_PATH, "logo.svg"))
self.tray = QSystemTrayIcon()
self.tray.setIcon(self.icon)
self.tray.setVisible(True)
self.tray.show()
self.tray_menu = QMenu()
self.title_action = QAction("𝗤𝘂𝗲𝗧𝘂𝗲𝗗𝘂𝗲")
self.title_action.setIcon(QIcon(os.path.join(ICON_PATH, "logo.svg")))
self.open_app_tray_action = QAction("𝗢𝗽𝗲𝗻 𝗳𝘂𝗹𝗹 𝗮𝗽𝗽")
self.open_app_tray_action.triggered.connect(self.open_app)
self.open_app_tray_action.setIcon(QIcon(os.path.join(ICON_PATH, f"open_app_icon_{THEME}.png")))
self.quit_app_tray_action = QAction("𝗤𝘂𝗶𝘁 𝗤𝘂𝗲𝗧𝘂𝗲𝗗𝘂𝗲")
self.quit_app_tray_action.triggered.connect(self.quit_app)
self.quit_app_tray_action.setIcon(QIcon(os.path.join(ICON_PATH, f"quit_app_icon_{THEME}.png")))
self.tray.setContextMenu(self.tray_menu)
# Toolbar
self.toolbar = QToolBar("Utilities")
self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
self.add_action = QAction(QIcon(os.path.join(ICON_PATH, f"add_task_icon_{THEME}.png")), "Add", self)
self.add_action.setStatusTip("Add a new task")
self.add_action.triggered.connect(lambda: self.change_page(self.add_page, "Add a Task", self.add_action))
self.del_action = QAction(QIcon(os.path.join(ICON_PATH, f"del_task_icon_{THEME}.png")), "Remove", self)
self.del_action.setStatusTip("Remove a task")
self.del_action.triggered.connect(lambda: self.change_page(self.del_page, "Remove a Task", self.del_action))
self.mark_all_as_done_action = QAction(
QIcon(os.path.join(ICON_PATH, f"mark_all_as_done_icon_{THEME}.png")), "Mark all as Done", self
)
self.mark_all_as_done_action.setStatusTip("Mark all items off as Done")
self.mark_all_as_done_action.triggered.connect(
lambda: self.change_page(self.mark_off_page, "Mark all as Done", self.mark_all_as_done_action)
)
self.del_done_action = QAction(
QIcon(os.path.join(ICON_PATH, f"del_done_icon_{THEME}.png")), "Remove Done", self
)
self.del_done_action.setStatusTip("Remove all tasks marked as done")
self.del_done_action.triggered.connect(
lambda: self.change_page(self.del_done_page, "Remove Done Tasks", self.del_done_action)
)
self.del_all_action = QAction(
QIcon(os.path.join(ICON_PATH, f"del_all_icon_{THEME}.png")), "Remove ALL Items", self
)
self.del_all_action.setStatusTip("Remove ALL ITEMS PERMANENTLY")
self.del_all_action.triggered.connect(
lambda: self.change_page(self.del_all_page, "Remove ALL Tasks", self.del_all_action)
)
self.toolbar.addAction(self.add_action)
self.toolbar.addAction(self.del_action)
self.toolbar.addAction(self.mark_all_as_done_action)
self.toolbar.addAction(self.del_done_action)
self.toolbar.addAction(self.del_all_action)
# Main layouts
self.window_layout = QVBoxLayout()
self.stack_layout = QStackedLayout()
self.main_layout = QVBoxLayout()
self.tasks_layout = QHBoxLayout()
self.todo_layout = QVBoxLayout()
self.todo_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.in_prog_layout = QVBoxLayout()
self.in_prog_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.done_layout = QVBoxLayout()
self.done_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Widgets
self.header_layout = QHBoxLayout()
self.header_layout.setSpacing(8)
self.header_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.header_menu_layout = QHBoxLayout()
self.header_title_layout = QHBoxLayout()
self.header_menu = QMenu()
self.header_menu_file = QAction("File")
self.header_menu_file.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_file)
self.header_menu_add = QAction("Add")
self.header_menu_add.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_add)
self.header_menu_remove = QAction("Remove")
self.header_menu_remove.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_remove)
self.header_menu_mark_off = QAction("Mark Off")
self.header_menu_mark_off.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_mark_off)
self.header_menu_edit = QAction("Edit")
self.header_menu_edit.setFont(QFont(self.families[4][0]))
self.header_menu.addAction(self.header_menu_edit)
self.header_menu_button = QPushButton()
self.header_menu_button.setIcon(QIcon(os.path.join(ICON_PATH, f"header_menu_icon_{THEME}.png")))
self.header_menu_button.setIconSize(QSize(24, 24))
self.header_menu_button.setFixedSize(48, 32)
self.header_menu_button.setFlat(True)
self.header_menu_button.setMenu(self.header_menu)
self.header_page_label = QLabel("Tasks")
self.header_page_label.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
self.header_page_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
self.header_page_label.setFont(QFont(self.families[4][0]))
self.header_menu_layout.addWidget(self.header_menu_button)
self.header_menu_layout.addWidget(self.header_page_label)
self.header_menu_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.header_label = QLabel("QueTueDue")
self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.header_label.setContentsMargins(0, 4, 0, 4)
self.header_label.setFont(QFont(self.families[4], 12))
self.header_sub_label = QLabel(f"{__version__}")
self.header_sub_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self.header_sub_label_palette = self.header_sub_label.palette()
self.header_sub_label_palette.setColor(QPalette.ColorRole.WindowText, QColor(50, 50, 50))
self.header_sub_label.setPalette(self.header_sub_label_palette)
self.header_sub_label.setFont(QFont(self.families[4], 10))
self.header_title_spacer = QSpacerItem(
self.header_menu_button.width() + self.header_page_label.width() + 4,
10,
QSizePolicy.Policy.Minimum,
QSizePolicy.Policy.Minimum,
)
self.header_title_spacer.setAlignment(Qt.AlignmentFlag.AlignRight)
self.header_title_layout.addWidget(self.header_label)
self.header_title_layout.addWidget(self.header_sub_label)
self.header_title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
self.header_layout.addLayout(self.header_menu_layout)
self.header_layout.addLayout(self.header_title_layout)
self.header_layout.addItem(self.header_title_spacer)
self.header_separator = separator("h")
self.window_layout.addLayout(self.header_layout)
self.window_layout.addWidget(self.header_separator)
self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.tasks_layout.addLayout(self.todo_layout)
self.tasks_layout.addWidget(separator("v"))
self.tasks_layout.addLayout(self.in_prog_layout)
self.tasks_layout.addWidget(separator("v"))
self.tasks_layout.addLayout(self.done_layout)
self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.in_prog_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.done_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.main_layout.addLayout(self.tasks_layout)
self.header_spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.main_layout.addItem(self.header_spacer)
self.todo_header = QLabel("To-Do")
try:
self.todo_header.setFont(QFont(self.families[0], 32))
except IndexError:
self.todo_header.setFont(QFont("", 32))
self.todo_layout.addWidget(self.todo_header)
self.todo_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.in_prog_header = QLabel("In Prog.")
try:
self.in_prog_header.setFont(QFont(self.families[0], 32))
except IndexError:
self.in_prog_header.setFont(QFont("", 32))
self.in_prog_layout.addWidget(self.in_prog_header)
self.in_prog_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.done_header = QLabel("Done :)")
try:
self.done_header.setFont(QFont(self.families[0], 32))
except IndexError:
self.done_header.setFont(QFont("", 32))
self.done_layout.addWidget(self.done_header)
self.done_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.load_checkboxes()
container = QWidget()
container.setLayout(self.main_layout)
self.setCentralWidget(container)
# Pages
self.main_page = QWidget()
self.main_page.setLayout(self.main_layout)
self.stack_layout.addWidget(self.main_page)
self.add_page = AddWindow(self)
self.stack_layout.addWidget(self.add_page)
self.del_page = DelWindow(self)
self.stack_layout.addWidget(self.del_page)
self.mark_off_page = MarkAllAsDoneWindow(self)
self.stack_layout.addWidget(self.mark_off_page)
self.del_done_page = DelDoneWindow(self)
self.stack_layout.addWidget(self.del_done_page)
self.del_all_page = DelAllWindow(self)
self.stack_layout.addWidget(self.del_all_page)
self.change_page(self.main_page, "Tasks", False)
self.window_layout.addLayout(self.stack_layout)
self.window_wrapper = QWidget()
self.window_wrapper.setLayout(self.window_layout)
self.setCentralWidget(self.window_wrapper)
def clear_layout(self, layout, start):
"""Removes all widgets in a given layout."""
for i in reversed(range(start, layout.count())):
task = layout.takeAt(i)
widget = task.widget()
if widget:
widget.deleteLater()
def load_checkboxes(self):
"""Clears layouts and system tray list and re-adds checkboxes
from to-do.txt.
"""
# Clear checkboxes
self.clear_layout(self.todo_layout, 1)
self.clear_layout(self.in_prog_layout, 1)
self.clear_layout(self.done_layout, 1)
# Clear and re-add static tray tasks (except open full app).
self.tray_menu.clear()
self.tray_actions = []
self.tray_menu.addAction(self.title_action)
self.tray_menu.addSeparator()
with open(TODO_PATH, "r") as f:
checkbox = ""
for line in f.readlines():
if not line.strip():
continue
self.blockSignals(True)
task_text = line[1:].strip()
checkbox = QCheckBox(task_text)
max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
checkbox.setMaximumWidth(max_width)
checkbox.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
try:
checkbox.setFont(QFont(self.families[4]))
except IndexError:
continue
# Add checkboxes and tray tasks depending on category
if line.startswith("t") or line.startswith("i"):
checkbox.setCheckState(Qt.CheckState.Unchecked)
self.todo_layout.addWidget(checkbox)
checkbox_tray_action = QAction(task_text)
self.tray_actions.append(checkbox_tray_action)
self.tray_menu.addAction(checkbox_tray_action)
elif line.startswith("i"):
checkbox.setCheckState(Qt.CheckState.PartiallyChecked)
self.in_prog_layout.addWidget(checkbox)
elif line.startswith("d"):
checkbox.setCheckState(Qt.CheckState.Checked)
self.done_layout.addWidget(checkbox)
else:
continue
self.blockSignals(False)
checkbox.setProperty("task", task_text)
checkbox.setTristate(True)
checkbox.stateChanged.connect(lambda state, cb=checkbox: self.moveCheckbox(cb, state))
# Add final static tray tasks and refresh the tray context menu
self.tray_menu.addSeparator()
self.tray_menu.addAction(self.open_app_tray_action)
self.tray_menu.addAction(self.quit_app_tray_action)
self.tray.setContextMenu(self.tray_menu)
def closeEvent(self, e):
"""Override the close signal and hide the window instead of
closing it, meaning that the process is not killed and the
system tray icon remains.
"""
if HIDE_WHEN_CLOSED == "True":
e.ignore()
self.hide()
def open_popup_window(self, WindowInstance, checked=False):
"""Open the specified popup window (WindowInstance). This is used for the
toolbar and button actions."""
self.w = WindowInstance(self)
self.w.show()
def open_app(self):
"""Open the main app when the Open full app system tray context
menu option is triggered.
"""
self.show()
def quit_app(self):
"""Quit the whole process when the Quit QueTueDue system tray
context menu option is triggered.
"""
sys.exit()
def moveCheckbox(self, cb, state):
"""Deletes specified task (cb) from to-do.txt and re-adds it
with a new prefix based on the state (state) of the checkbox.
"""
text = cb.text()
prefixes = (f"t{text}", f"i{text}", f"d{text}")
if state == 0:
self.todo_layout.addWidget(cb)
with open(TODO_PATH, "r") as f:
lines = f.readlines()
lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]
lines = [line for line in lines if line.strip()]
with open(TODO_PATH, "w", encoding="utf-8") as f:
f.writelines(lines)
with open(TODO_PATH, "a", encoding="utf-8") as f:
f.write(f"\nt{text}")
elif state == 1:
max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
cb.setMaximumWidth(max_width)
cb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.in_prog_layout.addWidget(cb)
with open(TODO_PATH, "r") as f:
lines = f.readlines()
lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]
lines = [line for line in lines if line.strip()]
with open(TODO_PATH, "w", encoding="utf-8") as f:
f.writelines(lines)
with open(TODO_PATH, "a", encoding="utf-8") as f:
f.write(f"\ni{text}")
elif state == 2:
max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
cb.setMaximumWidth(max_width)
cb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.done_layout.addWidget(cb)
with open(TODO_PATH, "r") as f:
lines = f.readlines()
lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]
with open(TODO_PATH, "w", encoding="utf-8") as f:
f.writelines(lines)
with open(TODO_PATH, "a", encoding="utf-8") as f:
f.write(f"\nd{text}")
def resizeEvent(self, event):
"""Override the default resizeEvent to calculate and adjust
checkbox label widths accordingly.
"""
self.load_checkboxes()
self.update_header_spacer()
super().resizeEvent(event)
def update_header_spacer(self):
"""Calculate and resize the width on the header spacer
(header_title_spacer)
"""
print(self.header_menu_button.width())
print(self.header_page_label.width())
print(self.header_menu_button.width() + self.header_page_label.width() + 4)
width = self.header_menu_button.width() + self.header_page_label.width() + 4
self.header_title_spacer.changeSize(
width,
1,
QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Minimum,
)
self.header_layout.invalidate()
def change_page(self, page, header_label, item=False):
"""Changes the current widget on the stack layout
(self.stack_layout).
"""
if self.stack_layout.currentWidget() == page:
self.stack_layout.setCurrentWidget(self.main_page)
self.header_page_label.setText("Tasks")
return
self.stack_layout.setCurrentWidget(page)
self.header_page_label.setText(header_label)
self.update_header_spacer()