How a QHBoxLayout could remove itself? - python

I have a code that makes some QHBoxLayouts with some elements in them. I want to remove each Row class (QHBoxLayouts object) when clicking on their own remove button.
Here is the code for testing it
from sys import exit, argv
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QPushButton, QLineEdit
class MainWindow(QWidget):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self.sub_window = None
self.screenWidth = 1920
self.screenHeight = 1080
self.windowWidth = 1000
self.windowHeight = 800
self.setupUi()
self.show()
def setupUi(self) -> None:
self.setWindowTitle("Password Manager")
self.setGeometry((self.screenWidth - self.windowWidth) // 2, (self.screenHeight - self.windowHeight) // 2, self.windowWidth, self.windowHeight)
self.mainLayout = QVBoxLayout()
self.scrollAreaLayoutContents = QVBoxLayout()
self.scrollArea = QScrollArea()
self.scrollArea.setLayout(self.scrollAreaLayoutContents)
for i in range(10):
self.scrollAreaLayoutContents.addLayout(Row(f"{i}"))
self.mainLayout.addWidget(self.scrollArea)
self.setLayout(self.mainLayout)
class Row(QHBoxLayout):
def __init__(self, name:str) -> None:
super().__init__()
self.nameLineEdit = QLineEdit(f"{name}")
self.nameLineEdit.setDisabled(True)
self.addWidget(self.nameLineEdit)
self.removeButton = QPushButton("remove")
self.removeButton.clicked.connect(self.removeItSelf)
self.addWidget(self.removeButton)
def removeItSelf(self) -> None:
self.removeItem(self)
if __name__ == "__main__":
app = QApplication(argv)
window = MainWindow()
exit(app.exec())
I want to remove each Row class (QHBoxLayouts object) when clicking on their own remove button.

self.removeItem(self) will obviously not work, because removeItem() removes an item from the layout represented by self, and an item cannot remove itself from itself.
The removal should be done on the level of the parent layout, but there are three aspects that should be kept in mind:
there is no direct way to get the parent layout from a nested one;
removing the layout does not remove the widgets;
the layout could have other nested layout of its own;
The first point can be easily solved when talking about removal: since QLayout inherits from QObject, we can call deleteLater().
In your simple case, assuming that the contents are known and do not change, it could be done like this:
def removeItSelf(self) -> None:
self.nameLineEdit.deleteLater()
self.removeButton.deleteLater()
self.deleteLater()
In any other situations (for instance, if contents are dynamically added), the only way to ensure that all the contents get destroyed is by using a recursive function that iterates all layout items.
While that is feasible, there is a much simpler solution: instead of using a layout subclass, use a basic QWidget as a container. In that case, all widgets are children of the container, and deleting the container will automatically take care of their removal:
class Row(QWidget):
def __init__(self, name:str) -> None:
super().__init__()
layout = QHBoxLayout(self)
self.nameLineEdit = QLineEdit(f"{name}")
self.nameLineEdit.setDisabled(True)
layout.addWidget(self.nameLineEdit)
self.removeButton = QPushButton("remove")
self.removeButton.clicked.connect(self.removeItSelf)
layout.addWidget(self.removeButton)
def removeItSelf(self) -> None:
self.deleteLater()
class MainWindow(QWidget):
# ...
def setupUi(self) -> None:
# ...
for i in range(10):
self.scrollAreaLayoutContents.addWidget(Row(f"{i}"))

Related

Window Manager parent-children relationship weird behavior in python PyQt5

What I want to achieve in python PyQt5 is when I open the script I open a parent window automatically with a button on the middle of the screen. When I click this button a new window suppose to open, and when I close the window I can close separately the parent window and the child window. I want to be able to close the parent window first. I've done that and it works but there is a weird behavior when I open more than 10 windows sometimes when I close a window it recreates another window by itself, and sometimes one of the windows closes all of the other opened windows. How to fix this issue, the code has no errors, but it does not work as intended.
I've tried to convert the first child window to a parent window, and if all parent windows are closed the remaining child windows to become parent. This is somewhat successful, but not what I want. Here is the code:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QAction, QFileDialog
from PyQt5.QtGui import QKeySequence
from PyQt5.QtCore import Qt
class WindowManager(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QPushButton("Create New Window", self)
self.button.clicked.connect(self.new_window)
self.setCentralWidget(self.button)
self.children = []
if self.parent() is not None and len(self.parent().children) > 0 and self.parent().children[0] == self:
self.is_first_child = True
else:
self.is_first_child = False
def new_window(self):
window_manager = WindowManager(self)
window_manager.resize(self.size())
window_manager.setWindowModality(Qt.NonModal)
window_manager.setWindowFlag(Qt.Window)
window_manager.create_new_window_on_close = True
self.children.append(window_manager)
window_manager.show()
def closeEvent(self, event):
if self.parent() is None:
if self.children:
if all(child.parent() is not None for child in self.children):
self.children[0].setParent(None)
self.children[0].setWindowModality(Qt.NonModal)
self.children[0].setWindowFlag(Qt.Window)
self.children[0].show()
self.children[0].children = self.children[1:]
self.close()
else:
if self in self.parent().children:
self.parent().children.remove(self)
self.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
window_manager = WindowManager()
window_manager.show()
sys.exit(app.exec_())
Code 2 :
import sys
from random import randint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QMenu,
QMenuBar,
QPushButton,
QVBoxLayout,
QWidget,
)
class AnotherWindow(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.label = QLabel("Another Window % d" % randint(0, 100))
layout.addWidget(self.label)
self.setLayout(layout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.windows1 = []
menubar = self.menuBar()
self.file_menu = menubar.addMenu("File")
self.button1 = QPushButton("New")
self.button1.clicked.connect(self.create_window1)
self.file_menu.addAction(self.button1.text(), lambda: self.create_window1())
def create_window1(self):
window = QMainWindow(None)
window.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
menubar = window.menuBar()
menubar.addMenu(self.file_menu)
self.windows1.append(window)
window.show()
def closeEvent(self, event):
event.accept()
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
I rewrote your script to fix the bugs, but I removed some unused parts:
is_first_child flag
create_new_window_on_close flag
Unused QKeySequence, QAction and QFileDialog imports
The main problem I found about was the WindowManager.children reference which messed up in the code. From what I could understand, only the parent should contain the current reference to the children list with updated data. Whenever the parent would be closed, this updated reference should be passed into one of the children.
The main flaw was in this line:
def new_window(self):
# ... code ...
self.children.append(window_manager)
# ... code ...
You're always inserting a new child into the current's instance reference of child. That is, if you click on the child QPushButton, you're never updating the parent's children list. You're always updating the current instance one.
In short words: it got confusing. So I reworked that into using a single shared list between all instances of WindowManager. Whenever a parent window is created, it creates it's own list. Whenever a child is created, it acquires the parent's list reference.
Here's my version of your script:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from PyQt5.QtCore import Qt
from random import randint
# Store taken indexes
taken = []
# If you're on Windows, this might not work unless you use another terminal
# application that allows ansi colors. (default windows console does not
# support it)
red = '\033[91m%s\033[0m'
green = '\033[92m%s\033[0m'
# With this we can open 1000 different windows on our testing environment
# ensuring each window contains a different index value
def uniqueIndex():
idx = randint(0, 1000)
while (idx in taken):
idx = randint(0, 1000)
taken.append(idx)
return idx
class WindowManager(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# Get an unique identifier (only for testing)
self.index = uniqueIndex()
# If this instance is the parent, create a new shared reference.
if (parent is None):
self.children = []
# Otherwise, acquire the reference from the parent window, and add
# itself inside it.
else:
self.children = parent.children
# print on terminal that we're adding a window
print('%s %d' % (green % 'Adding: ', self.index))
# Update the window title, and add it to the shared children list
self.update_parent(parent)
self.children.append(self)
self.button = QPushButton("Create New Window", self)
self.button.clicked.connect(self.new_window)
self.setCentralWidget(self.button)
self.setMinimumWidth(300)
def new_window(self):
window_manager = WindowManager(self)
window_manager.resize(self.size())
window_manager.setWindowModality(Qt.NonModal)
window_manager.setWindowFlag(Qt.Window)
window_manager.show()
# Used just for testing.
def update_parent(self, parent=None):
if (parent is None):
self.setWindowTitle('Parent %d' % self.index)
else:
self.setWindowTitle('Parent %d: Index %d' % (parent.index, self.index))
self.setParent(parent)
def closeEvent(self, event):
# Remove the self from the shared children list
self.children.remove(self)
# print on the terminal that we're removing a window
print('%s: %d' % (red % 'Removing:', self.index))
# If the array is not empty:
if (self.children):
# Reparent all children based on the current parent (which can be None)
for child in self.children:
if (child.parent() == self):
child.update_parent(self.parent())
# set parent clears all window flags.
child.setWindowModality(Qt.NonModal)
child.setWindowFlag(Qt.Window)
# ensure the child is visible
child.show()
# Ensure we're closing the current window.
self.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
window_manager = WindowManager()
window_manager.show()
sys.exit(app.exec_())

How do I connect three or more windows in PyQt

I have a complex program in which I need to connect multiple windows. Unfortunately, I seem to be not fully understanding the concept/steps necessary to do this so bonus points if anyone can explain the steps/process well. In my current program, I have a list of items. Once I select them by moving them over to the right list widget, I need them to go to the third window. The third window should be activated by clicking the dots on the second window. The program runs and shows the second window appropriately but the signal/slot connection of the dots button does not work. However, the rest of the code is working because if I switch the toolkit to show the third window, that part is performing as expected. My code is below, and again, no errors are being returned, but clicking the dots button on the second window does nothing.
Also, a question - do I instantiate the third window within the second class, or only within the main window? Again, struggling to fully understand the process and I will need to do this multiple more times.
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTextEdit, QGridLayout, QHBoxLayout, QVBoxLayout, QSizePolicy, QFileDialog, QTabWidget, QCheckBox
import PyQt5.QtGui as qtg
import glob
import os
from PyQt5.QtCore import Qt, QSettings
import inspect
from PyQt5 import QtCore
import pandas as pd
import pathlib
import pyreadstat
import json
class ThirdWindow(QWidget):
def __init__(self):
super().__init__()
self.layout = QGridLayout()
self.setLayout(self.layout)
self.allVariables = QListWidget()
self.variablesSelected = QListWidget()
#self.allVariables.insertItem(0, 'Hello')
self.layout.addWidget(self.allVariables, 1,0)
self.layout.addWidget(self.variablesSelected, 1, 1)
def setItems(self, items):
self.allVariables.clear()
for item in items:
self.allVariables.addItem(item)
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
##not sure if I am supposed to instantiate this here or only in the main window class
self.thirdWindow = ThirdWindow()
self.layout = QGridLayout(self)
self.by = QLabel("By")
self.byVariables = QLineEdit()
self.byButton = QPushButton("...")
self.layout.addWidget(self.by, 1, 0)
self.layout.addWidget(self.byVariables, 2, 0)
self.layout.addWidget(self.byButton, 2, 1)
def seconddWindowConnections(self):
self.byButton.clicked.connect(self.show_third_window)
#self.buttons['Toolkit'].clicked.connect(self.show_new_window)
def show_third_window(self):
self.thirdWindow.show()
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# Add a title
self.setWindowTitle("GUI Querying Program")
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.initUI()
self.setButtonConnections()
self.sw = SecondWindow()
self.tw = ThirdWindow()
def initUI(self):
subLayouts = {}
subLayouts['LeftColumn'] = QGridLayout()
self.layout.addLayout(subLayouts['LeftColumn'],1)
# Buttons
self.buttons = {}
self.buttons['addVariable'] = QPushButton('>')
self.buttons['removeVariable'] = QPushButton('<')
self.buttons['Toolkit'] = QPushButton('Toolkit')
self.variables = QListWidget()
self.selectedVariables = QListWidget()
subLayouts['LeftColumn'].addWidget(self.variables, 7,0,4,1)
subLayouts['LeftColumn'].addWidget(self.selectedVariables, 7,1,4,1)
subLayouts['LeftColumn'].addWidget(self.buttons['addVariable'], 10,0,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['removeVariable'], 10,1,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['Toolkit'], 11,1,1,1)
names = ['apple', 'banana', 'Cherry']
self.variables.insertItems(0, names)
def setButtonConnections(self):
self.buttons['addVariable'].clicked.connect(self.add_variable)
self.buttons['Toolkit'].clicked.connect(self.show_new_window)
self.buttons['Toolkit'].clicked.connect(self.add_selected_variables)
def add_variable(self):
for item in self.variables.selectedItems():
self.selectedVariables.addItem(item.clone())
def show_new_window(self):
self.sw.show()
def add_selected_variables(self):
items = []
for i in range(self.selectedVariables.count()):
items.append(self.selectedVariables.item(i).clone())
self.tw.setItems(items)
if __name__ == "__main__":
import sys
app = QApplication([])
mw = MainWindow()
mw.show()
app.exec()
The main issue with your code is that secondWindowConnections is never called so the button actually does nothing. I corrected that and fixed a few other issues I found in my example below. I left out the bits where I made no changes and all the changes I did make I made inline notes explaining them:
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
self.thirdWindow = None # dont initialize until neccessary
self.thirdWindowItems = []
self.layout = QGridLayout(self)
self.by = QLabel("By")
self.byVariables = QLineEdit()
self.byButton = QPushButton("...")
self.layout.addWidget(self.by, 1, 0)
self.layout.addWidget(self.byVariables, 2, 0)
self.layout.addWidget(self.byButton, 2, 1)
self.secondWindowConnections() # Run this to setup the
# signal for the third window.
def secondWindowConnections(self): # this had a typo
self.byButton.clicked.connect(self.show_third_window)
def show_third_window(self):
if self.thirdWindow is None: # if window has been created yet
self.thirdWindow = ThirdWindow() # create window
if not self.thirdWindow.isVisible(): # if window is showing
self.thirdWindow.show() # show window
self.thirdWindow.setItems(self.thirdWindowItems) # send items to window
def send_items(self, items): # this is to collect the variable that
self.thirdWindowItems = items # move to the third window
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# Add a title
self.setWindowTitle("GUI Querying Program")
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.initUI()
self.setButtonConnections()
self.sw = None # dont initialize until neccessary.
def initUI(self):
subLayouts = {}
subLayouts['LeftColumn'] = QGridLayout()
self.layout.addLayout(subLayouts['LeftColumn'],1)
self.buttons = {}
self.buttons['addVariable'] = QPushButton('>')
self.buttons['removeVariable'] = QPushButton('<')
self.buttons['Toolkit'] = QPushButton('Toolkit')
self.variables = QListWidget()
self.selectedVariables = QListWidget()
subLayouts['LeftColumn'].addWidget(self.variables, 7,0,4,1)
subLayouts['LeftColumn'].addWidget(self.selectedVariables, 7,1,4,1)
subLayouts['LeftColumn'].addWidget(self.buttons['addVariable'], 10,0,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['removeVariable'], 10,1,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['Toolkit'], 11,1,1,1)
names = ['apple', 'banana', 'Cherry']
self.variables.insertItems(0, names)
def setButtonConnections(self):
self.buttons['addVariable'].clicked.connect(self.add_variable)
self.buttons['Toolkit'].clicked.connect(self.show_new_window)
# self.buttons['Toolkit'].clicked.connect(self.add_selected_variables)
# only use one connnect slot
def add_variable(self):
for item in self.variables.selectedItems():
self.selectedVariables.addItem(item.clone())
def show_new_window(self):
if self.sw is None: # check if window has been constructed
self.sw = SecondWindow() # construct window
if not self.sw.isVisible(): # If winow is not showing
self.sw.show() # show window
self.sw.send_items(self.add_selected_variables()) # send selected
# variables to second window
def add_selected_variables(self):
items = []
for i in range(self.selectedVariables.count()):
items.append(self.selectedVariables.item(i).clone())
# self.tw.setItems(items) ... self.tw doesnt exist so return them
return items

Move to next tab (and focus on the corresponding widget) when pressing the Tab key

In my app I have a QTabWidget which holds a variable number of seemingly "identical" tabs with a variable number of widgets.
I want, once the TAB (or shift-TAB) button is pressed, for the focus of the app to move to the next (or previous) tab, and focus on the corresponding widget of that tab (the one corresponding to the widget which had the focus until the key press).
What is the best way to go around this in a simple way? I tried using a QShortcut to catch the key-press but I can't seem to figure out a way to get the corresponding widget in the next or previous tab and focus on that.
Here's a minimal example of the code, which simply moves to the next tab but not to the corresponding widget:
import sys
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import *
class tabdemo(QTabWidget):
def __init__(self, num_tabs=2):
super().__init__()
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Tab), self)
shortcut.activated.connect(self.on_tab)
shortcut2 = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Backtab), self)
shortcut2.activated.connect(self.on_shift_tab)
self.tabs = []
for i in range(num_tabs):
newtab = QWidget()
self.tabs.append(newtab)
self.addTab(newtab, f'Tab {i}')
self.add_widgets_to(newtab)
def add_widgets_to(self, tab):
layout = QVBoxLayout()
tab.setLayout(layout)
layout.addWidget(QSpinBox())
layout.addWidget(QCheckBox())
gender = QHBoxLayout()
gender.addWidget(QRadioButton("Male"))
gender.addWidget(QRadioButton("Female"))
layout.addLayout(gender)
#QtCore.pyqtSlot()
def on_tab(self):
current_tab = self.currentIndex()
self.setCurrentIndex((current_tab + 1) % self.count())
# TODO find current widget in focus, and find the corresponding one in the next tab, and focus on that one... note that widgets could be complex (i.e., not direct children...)
#QtCore.pyqtSlot()
def on_shift_tab(self):
print("do_something")
current_tab = self.currentIndex()
self.setCurrentIndex((current_tab - 1) % self.count())
def main():
app = QApplication(sys.argv)
ex = tabdemo()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Since the OP indicates that each page will have identical components then an index can be associated so that the index of the tab can be obtained before changing it and then set the widget's focus then set the focus to the other corresponding widget.
import sys
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QApplication,
QCheckBox,
QHBoxLayout,
QRadioButton,
QShortcut,
QSpinBox,
QTabWidget,
QVBoxLayout,
QWidget,
)
class Page(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
spinbox = QSpinBox()
checkbox = QCheckBox()
male_radio = QRadioButton("Male")
female_radio = QRadioButton("Female")
layout = QVBoxLayout(self)
layout.addWidget(spinbox)
layout.addWidget(checkbox)
gender = QHBoxLayout()
gender.addWidget(male_radio)
gender.addWidget(female_radio)
layout.addLayout(gender)
for i, widget in enumerate((spinbox, checkbox, male_radio, female_radio)):
widget.setProperty("tab_index", i)
class Tabdemo(QTabWidget):
def __init__(self, num_tabs=2):
super().__init__()
shortcut = QShortcut(QKeySequence(Qt.Key_Tab), self)
shortcut.activated.connect(self.next_tab)
shortcut2 = QShortcut(QKeySequence(Qt.Key_Backtab), self)
shortcut2.activated.connect(self.previous_tab)
for i in range(num_tabs):
page = Page()
self.addTab(page, f"Tab {i}")
#pyqtSlot()
def next_tab(self):
self.change_tab((self.currentIndex() + 1) % self.count())
#pyqtSlot()
def previous_tab(self):
self.change_tab((self.currentIndex() - 1) % self.count())
def change_tab(self, index):
focus_widget = QApplication.focusWidget()
tab_index = focus_widget.property("tab_index") if focus_widget else None
self.setCurrentIndex(index)
if tab_index is not None and self.currentWidget() is not None:
for widget in self.currentWidget().findChildren(QWidget):
i = widget.property("tab_index")
if i == tab_index:
widget.setFocus(True)
def main():
app = QApplication(sys.argv)
ex = Tabdemo()
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Building on eyllanesc's answer, I improved the functionality to:
Account for the scrollbar location (if exists)
Use a bi-directional dictionary (implemented here) instead of a linear lookup
Dynamically add all relevant widgets using the update_map() method instead of having to add each widget manually.
Posting in case anyone finds this useful.
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QWidget, QTabWidget, QShortcut, QApplication, QScrollArea
class BidirectionalDict(dict):
def __init__(self, *args, **kwargs):
super(BidirectionalDict, self).__init__(*args, **kwargs)
self.inverse = {}
for key, value in self.items():
self.inverse.setdefault(value, []).append(key)
def __setitem__(self, key, value):
if key in self:
self.inverse[self[key]].remove(key)
super(BidirectionalDict, self).__setitem__(key, value)
self.inverse.setdefault(value, []).append(key)
def __delitem__(self, key):
self.inverse.setdefault(self[key], []).remove(key)
if self[key] in self.inverse and not self.inverse[self[key]]:
del self.inverse[self[key]]
super(BidirectionalDict, self).__delitem__(key)
def get_first_inv(self, key):
return self.inverse.get(key, [None])[0]
class Page(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.widgets_map = BidirectionalDict()
# ... add your widgets ...
self.update_map()
def update_map(self):
widgets = self.findChildren(QWidget)
for i, widget in enumerate(widgets):
self.widgets_map[i] = widget
class MyQTabWidget(QTabWidget):
def __init__(self):
super().__init__()
shortcut = QShortcut(QKeySequence(Qt.Key_Tab), self)
shortcut.activated.connect(self.next_tab)
shortcut2 = QShortcut(QKeySequence(Qt.Key_Backtab), self)
shortcut2.activated.connect(self.previous_tab)
#pyqtSlot()
def next_tab(self):
self.change_tab((self.currentIndex() + 1) % self.count())
#pyqtSlot()
def previous_tab(self):
self.change_tab((self.currentIndex() - 1) % self.count())
def change_tab(self, new_tab_index):
old_tab: Page = self.currentWidget()
focus_widget = QApplication.focusWidget()
widget_index = old_tab.widgets_map.get_first_inv(focus_widget) if focus_widget else None
self.setCurrentIndex(new_tab_index)
new_tab: Page = self.currentWidget()
if new_tab is not None and widget_index is not None:
corresponding_widget: QWidget = new_tab.widgets_map[widget_index]
corresponding_widget.setFocus(True)
# Move scrollbar to the corresponding position
if hasattr(old_tab, 'scrollbar'):
# Tabs are identical so new_tab must have scrollbar as well
old_y = old_tab.scrollbar.verticalScrollBar().value()
scrollbar: QScrollArea = new_tab.scrollbar
scrollbar.verticalScrollBar().setValue(old_y)

QMenu does not execute methods properly

I am working with a custom QMenu which executes some methods. The menu has three options: a delete row option, a toggle variable option and a debug option, which prints the value of the toggleing variable. The code is not properly executed. Sometimes the debug button doesnt work and it suddely gets executed many times. The toggle option needs to be clicked twice to work, i dont know why. This is my MRE:
# -*- coding: utf-8 -*-
from PyQt5.QtCore import Qt, QRect, pyqtSlot
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QMainWindow, QLabel, QMenu, \
QApplication, QVBoxLayout, QListWidgetItem, QListWidget, QAction
class Punto(QWidget):
def __init__(self, parent, internal_id, name):
QWidget.__init__(self)
# Toggle variable
self.render = True
self.customContextMenuRequested.connect(self.context_menu)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.menu = QMenu()
self.borrar = QAction("Delete")
self.ver = QAction("Toggle")
self.debug = QAction("Debug")
self.ver.setCheckable(True)
self.ver.setChecked(True)
self.parent = parent
self.id = internal_id
label = QLabel(name)
hbox = QHBoxLayout()
hbox.addWidget(label)
hbox.addStretch(1)
self.setLayout(hbox)
def context_menu(self):
self.menu.addAction(self.borrar)
self.borrar.triggered.connect(self.delete)
self.menu.addAction(self.ver)
self.ver.triggered.connect(self.change)
self.menu.addAction(self.debug)
self.debug.triggered.connect(self.debugg)
self.menu.exec(QCursor.pos())
#pyqtSlot()
def debugg(self):
print(f"Render: {self.render}")
#pyqtSlot()
def change(self):
if self.ver.isChecked():
self.ver.setChecked(False)
self.render = False
else:
self.ver.setChecked(True)
self.render = True
#property
def itemid(self):
return self.id
#pyqtSlot()
def delete(self):
self.parent.delete_point(self.id)
class Ventana(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setFixedSize(200, 200)
widget_central = QWidget(self)
boton_punto = QPushButton(widget_central)
boton_punto.setGeometry(QRect(0, 0, 200, 20))
boton_punto.clicked.connect(self.crear_punto)
boton_punto.setText("Create")
widget_punto = QWidget(widget_central)
widget_punto.setGeometry(QRect(0, 20, 200, 200))
vertical_punto = QVBoxLayout(widget_punto)
vertical_punto.setContentsMargins(0, 0, 0, 0)
self.lista_puntos = QListWidget(widget_punto)
vertical_punto.addWidget(self.lista_puntos)
self.id_punto = 0
self.setCentralWidget(widget_central)
def crear_punto(self):
# Add placeholder item to List
item = QListWidgetItem()
self.lista_puntos.addItem(item)
# Create Custom Widget
punto = Punto(self, self.id_punto, "A")
self.id_punto += 1
item.setSizeHint(punto.minimumSizeHint())
# Set the punto widget to be displayed within the placeholder item
self.lista_puntos.setItemWidget(item, punto)
def delete_point(self, idd):
for indx in range(self.lista_puntos.count()):
item = self.lista_puntos.item(indx)
widget = self.lista_puntos.itemWidget(item)
if widget.id == idd:
self.lista_puntos.takeItem(self.lista_puntos.row(item))
break
if __name__ == "__main__":
MainEvent = QApplication([])
main_app = Ventana()
main_app.show()
MainEvent.exec()
You have 2 errors:
By default a QAction already makes the change of state so it is not necessary that you implement it, but you are doing it, that is, by default the QAction changes from on to off (or vice versa) but you by code change it from off a on (or vice versa) that when done in ms the change is not observed. So instead of connecting the triggered signal, use the toggled signal and just change the render.
When you connect a signal to the same slot "n" times the slot is invoked "n" times, and in your case you are connecting it every time the context_menu method is invoked, there are at least 2 solutions: make the connection only once or use the type of connection Qt::UniqueConnection, in my solution I will use the first one.
Considering the above, the solution is:
class Punto(QWidget):
def __init__(self, parent, internal_id, name):
QWidget.__init__(self)
# Toggle variable
self.render = True
self.customContextMenuRequested.connect(self.context_menu)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.menu = QMenu()
self.borrar = QAction("Delete")
self.ver = QAction("Toggle")
self.debug = QAction("Debug")
self.ver.setCheckable(True)
self.ver.setChecked(True)
self.parent = parent
self.id = internal_id
label = QLabel(name)
hbox = QHBoxLayout(self)
hbox.addWidget(label)
hbox.addStretch(1)
self.borrar.triggered.connect(self.delete)
self.ver.toggled.connect(self.change)
self.debug.triggered.connect(self.debugg)
self.menu.addAction(self.borrar)
self.menu.addAction(self.ver)
self.menu.addAction(self.debug)
def context_menu(self):
self.menu.exec(QCursor.pos())
#pyqtSlot()
def debugg(self):
print(f"Render: {self.render}")
#pyqtSlot(bool)
def change(self, state):
self.render = self.ver.isChecked()
#property
def itemid(self):
return self.id
#pyqtSlot()
def delete(self):
self.parent.delete_point(self.id)

How to use PyQt5 QCompleter for code completion

I want to create a QLineEdit field with basic code completion capability, but so far whenever I select an attribute of an item item.attr, the item. is replaced by attr rather than inserting attr after item.. Furthermore if that attr has attr.subattr, it is impossible to predict it because item. has been replaced and attr. does not exist at the root of my model.
I have created a relatively minimal example:
import sys
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QApplication,QWidget,QVBoxLayout,QLineEdit,QCompleter
test_model_data = [
('tree',[ # tree
('branch', [ # tree.branch
('leaf',[])]), # tree.branch.leaf
('roots', [])]), # tree.roots
('house',[ # house
('kitchen',[]), # house.kitchen
('bedroom',[])]), # house.bedroom
('obj3',[]), # etc..
('obj4',[])
]
class codeCompleter(QCompleter):
def splitPath(self, path):
return path.split('.') #split table.member
class mainApp(QWidget):
def __init__(self):
super().__init__()
self.entry = QLineEdit(self)
self.model = QStandardItemModel(parent=self)
self.completer = codeCompleter(self.model, self)
self.entry.setCompleter(self.completer)
layout = QVBoxLayout()
layout.addWidget(self.entry)
self.setLayout(layout)
self.update_model() #normally called from a signal when new data is available
def update_model(self):
def addItems(parent, elements):
for text, children in elements:
item = QStandardItem(text)
parent.appendRow(item)
if children:
addItems(item, children)
addItems(self.model, test_model_data)
if __name__ == "__main__":
app = QApplication(sys.argv)
hwind = mainApp()
hwind.show()
sys.exit(app.exec_())
I came up with this approach from the Qt5 Docs and an example with Qt4.6, but neither combine all of what I'm trying to accomplish. Do I need a different model structure? Do I need to subclass more of QCompleter? Do I need a different Qt class?
gif of example: (sorry for quality)
Epilogue:
For those interested in actual code completion, I expanded on my code after integrating #eyllanesc's answer so that text before the matched sequence of identifiers was left alone (text ahead of the matched sequence does not prevent matching, nor is deleted when a new match is inserted). All it took was a little bit of regex to separate the part we want to complete from the preceeding text:
class CodeCompleter(QCompleter):
ConcatenationRole = Qt.UserRole + 1
def __init__(self, parent=None, data=[]):
super().__init__(parent)
self.create_model(data)
self.regex = re.compile('((?:[_a-zA-Z]+\w*)(?:\.[_a-zA-Z]+\w*)*\.?)$')
def splitPath(self, path): #breaks lineEdit.text() into list of strings to match to model
match = self.regex.search(path)
return match[0].split('.') if match else ['']
def pathFromIndex(self, ix): #gets model node (QStandardItem) and returns "text" for lineEdit.setText(text)
return self.regex.sub(ix.data(CodeCompleter.ConcatenationRole), self.completionPrefix())
The pathFromIndex() method returns the string that will be placed in the QLineEdit, instead it will return the concatenation of the text of the item and the texts of its predecessors. To make it more efficient and not calculate that online concatenation, a new role will be created to the model that contains that data.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QCompleter
test_model_data = [
('tree',[ # tree
('branch', [ # tree.branch
('leaf',[])]), # tree.branch.leaf
('roots', [])]), # tree.roots
('house',[ # house
('kitchen',[]), # house.kitchen
('bedroom',[])]), # house.bedroom
('obj3',[]), # etc..
('obj4',[])
]
class CodeCompleter(QCompleter):
ConcatenationRole = Qt.UserRole + 1
def __init__(self, data, parent=None):
super().__init__(parent)
self.create_model(data)
def splitPath(self, path):
return path.split('.')
def pathFromIndex(self, ix):
return ix.data(CodeCompleter.ConcatenationRole)
def create_model(self, data):
def addItems(parent, elements, t=""):
for text, children in elements:
item = QStandardItem(text)
data = t + "." + text if t else text
item.setData(data, CodeCompleter.ConcatenationRole)
parent.appendRow(item)
if children:
addItems(item, children, data)
model = QStandardItemModel(self)
addItems(model, data)
self.setModel(model)
class mainApp(QWidget):
def __init__(self):
super().__init__()
self.entry = QLineEdit(self)
self.completer = CodeCompleter(test_model_data, self)
self.entry.setCompleter(self.completer)
layout = QVBoxLayout()
layout.addWidget(self.entry)
self.setLayout(layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
hwind = mainApp()
hwind.show()
sys.exit(app.exec_())

Categories

Resources