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)
Related
Grееtings аll. I am new to this site, so go easy on me.
I am building a program in python using PyQt5 for the interface. I currently have, among other things, a QListWidget (list window) into which I insert a number of QWidgets (list items) through a function during the running of the program. I have implemented an eventFilter by subclassing QObject, and I use it to differentiate between left and right click, and from that I send to the controller class to handle one or the other click accordingly.
I have made it so that when right-clicked on a list item, a context menu appears. However, I also need a context menu to appear when the list window is clicked (not on a list item). The problem which occurs is that when right-clicked on a list item, both the list item context menu and list window context menu appear. This must be because the event filter recognises the click as occurring within the list window, because it is occurring on a list item, which is within the list window. What I need is that when right-clicked on a list item, only its context menu appears, and similarly for the list window, when right-clicked outside the list items.
I have tried checking if source equals the widget where the event appeared, but it seems to recognise both widgets' events independently and if I gate the call to the handler with an if condition, one of the handlers never receives a call. I have searched around the web and this site, but I have found nothing of use. Perhaps it is due to me not being a native speaker and so not being able to phrase things correctly (you can see how clunky my phrasing is).
Below follows some code extracted from my program. Note that anything irrelevant has been cut away to make for a minimal example. For your convenience, I have also merged the GUI files into one and did the same for the control files. I have tested this minimal example and it reproduces the problem. It could not get smaller, so if you deem the code listed below too long, notify me and I can reupload it to GitHub if it is allowed to show the minimal example that way instead of putting code into the question directly.
custom_classes.py:
from PyQt5.QtCore import Qt, QEvent, QObject
class MyFilter(QObject):
def __init__(self, parent, ctrl):
super().__init__(parent)
self._parent = parent
self.ctrl = ctrl
self.parent.installEventFilter(self)
#property
def parent(self):
return self._parent
def eventFilter(self, source, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
self.ctrl.handle_left_click()
elif event.button() == Qt.RightButton:
self.ctrl.handle_right_click(event)
return super().eventFilter(source, event)
gui.py:
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QListWidget
from PyQt5.QtWidgets import QScrollArea
from PyQt5.QtCore import Qt
class MainFrame(QWidget):
def __init__(self):
super().__init__()
self.main_layout = QHBoxLayout()
self.setLayout(self.main_layout)
class ListItemFrame(QWidget):
def __init__(self):
super().__init__()
self.main = QHBoxLayout()
self.main.setContentsMargins(0,0,0,0)
self.name_layout = QHBoxLayout()
self.name_layout.setContentsMargins(0,0,0,0)
self.main.addLayout(self.name_layout)
self.name = QLabel("")
self.name.setMaximumHeight(20)
self.name_layout.addWidget(self.name)
self.setLayout(self.main)
class ListFrame(QListWidget):
def __init__(self):
super().__init__()
self.main = QVBoxLayout()
self.scroll_widget = QScrollArea()
self.scroll_widget.setWidgetResizable(True)
self.scroll_layout = QVBoxLayout()
self.scroll_layout.setAlignment(Qt.AlignTop)
self.scroll_layout_widget = QWidget()
self.scroll_layout_widget.setLayout(self.scroll_layout)
self.scroll_widget.setWidget(self.scroll_layout_widget)
self.main.addWidget(self.scroll_widget)
self.setLayout(self.main)
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
main.py:
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QStackedLayout
from PyQt5.QtWidgets import QWidget
from gui import MainFrame
from ctrl import Controller
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("minimal example")
self.stacked = QStackedLayout()
self.main_page = MainFrame()
self.stacked.addWidget(self.main_page)
self.setLayout(self.stacked)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = Window()
window.show()
c = Controller(window, app)
sys.exit(app.exec())
To reiterate, the context menu appears for both the list item and the list window when a list item is right-clicked. What I need is for it to appear only for the list item if a list item is right-clicked.
Edit: seems the site bit off a part of my introduction. Readded it!
this is probably not the best way to do it but it works. You just create a global variable, for example list_element_clicked and when you click "hello" (of course not quit because you are going to exit the window and there is no point) you set that variable to True. If that variable is False, you show the ListControl context menu, and if not, you set that variable to True, so next time if you click on your ListControl it will appear, and if you click on ListItem it will not.
Finally there is an extra case, if you don't click anywhere after clicking on ListItem, nothing will happen (the ListControl is not shown and the variable is not changed) so everything will work perfectly next time.
So here is the code:
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
list_element_clicked = False
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
global list_element_clicked
if(list_element_clicked == False):
self.show_options(event)
else:
list_element_clicked = False
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
global list_element_clicked
list_element_clicked = True
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
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)
I'm trying to add custom animation to QPushbutton without making a custom QPushbutton and overriding its enterEvent() and leaveEvent().
So far I've tried this,
#staticmethod
def addButtonHoverAnimation(button:QPushButton,currentPos:QPoint):
'''
Method to:
=> Add hover animation for provided button
'''
enterShift = QPropertyAnimation(button,b'pos',button)
exitShift = QPropertyAnimation(button,b'pos',button)
def enterEvent(e):
pos=button.pos()
enterShift.setStartValue(pos)
enterShift.setEndValue(QPoint(pos.x()+3,pos.y()+3))
enterShift.setDuration(100)
enterShift.start()
Effects.dropShadow(button,1,2)
def leaveEvent(e):
pos=button.pos()
exitShift.setStartValue(pos)
exitShift.setEndValue(QPoint(pos.x()-3,pos.y()-3))
exitShift.setDuration(100)
exitShift.start()
Effects.dropShadow(button)
button.enterEvent=enterEvent
button.leaveEvent=leaveEvent
But when I move the mouse very quickly in and out of the button before the animation finishes, The button starts to move wierdly towards the North-West direction.
Button Animation Using Dynamic Positions
I figured out this was due to the leaveEvent() being triggered before enterEvent() even finishes and also because the start and end values are dynamic. So, I tried providing currentPos as a static position and using it instead,
#staticmethod
def addButtonHoverAnimation(button:QPushButton,currentPos:QPoint):
'''
Method to:
=> Add hover animation for provided button
'''
enterShift = QPropertyAnimation(button,b'pos',button)
enterShift.setStartValue(currentPos)
enterShift.setEndValue(QPoint(currentPos.x()+3,currentPos.y()+3))
enterShift.setDuration(100)
exitShift = QPropertyAnimation(button,b'pos',button)
exitShift.setStartValue(QPoint(currentPos.x()-3,currentPos.y()-3))
exitShift.setEndValue(currentPos)
exitShift.setDuration(100)
def enterEvent(e):
button.setProperty(b'pos',exitShift.endValue())
enterShift.start()
Effects.dropShadow(button,1,2)
def leaveEvent(e):
exitShift.start()
Effects.dropShadow(button)
button.enterEvent=enterEvent
button.leaveEvent=leaveEvent
On running, as soon as the mouse enters the QPushbutton, it moves to the top-left of its parent widget and the animation starts working fine. I can't figure out why this is happening. But I was able to get that, it only happened when I used any static value in the animation.
Button Animation with Static Position:
Here is an example:
import sys
from PyQt5.QtCore import QEvent, QPoint, QObject, QPropertyAnimation
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
# This is the same method mentioned above
from styling import addButtonHoverAnimation
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout=QVBoxLayout()
button1 = QPushButton("Proceed1", self)
layout.addWidget(button1)
button2 = QPushButton("Proceed2", self)
layout.addWidget(button2)
self.setLayout(layout)
self.resize(640, 480)
addButtonHoverAnimation(button1)
addButtonHoverAnimation(button2)
def main():
app = QApplication(sys.argv)
view = Widget()
view.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
The problem is that probably when the state is changed from enter to leave (or vice versa) the previous animation still does not end so the position of the widget is not the initial or final position, so when starting the new animation there is a deviation that accumulates. One possible solution is to initialize the position and keep it as a reference.
On the other hand you should not do x.fooMethod = foo_callable since many can fail, in this case it is better to use an eventfilter.
import sys
from dataclasses import dataclass
from functools import cached_property
from PyQt5.QtCore import QEvent, QPoint, QObject, QPropertyAnimation
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget
#dataclass
class AnimationManager(QObject):
widget: QWidget
delta: QPoint = QPoint(3, 3)
duration: int = 100
def __post_init__(self):
super().__init__(self.widget)
self._start_value = QPoint()
self._end_value = QPoint()
self.widget.installEventFilter(self)
self.animation.setTargetObject(self.widget)
self.animation.setPropertyName(b"pos")
self.reset()
def reset(self):
self._start_value = self.widget.pos()
self._end_value = self._start_value + self.delta
self.animation.setDuration(self.duration)
#cached_property
def animation(self):
return QPropertyAnimation(self)
def eventFilter(self, obj, event):
if obj is self.widget:
if event.type() == QEvent.Enter:
self.start_enter_animation()
elif event.type() == QEvent.Leave:
self.start_leave_animation()
return super().eventFilter(obj, event)
def start_enter_animation(self):
self.animation.stop()
self.animation.setStartValue(self.widget.pos())
self.animation.setEndValue(self._end_value)
self.animation.start()
def start_leave_animation(self):
self.animation.stop()
self.animation.setStartValue(self.widget.pos())
self.animation.setEndValue(self._start_value)
self.animation.start()
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button1 = QPushButton("Proceed1", self)
button1.move(100, 100)
button2 = QPushButton("Proceed2", self)
button2.move(200, 200)
self.resize(640, 480)
animation_manager1 = AnimationManager(widget=button1)
animation_manager2 = AnimationManager(widget=button2)
def main():
app = QApplication(sys.argv)
view = Widget()
view.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
184 / 5000
Resultados de traducción
If you are using a layout then you must reset the position since the layout does not apply the position change immediately but only when the parent widget applies the changes.
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button1 = QPushButton("Proceed1")
button2 = QPushButton("Proceed2")
lay = QVBoxLayout(self)
lay.addWidget(button1)
lay.addWidget(button2)
self.resize(640, 480)
self.animation_manager1 = AnimationManager(widget=button1)
self.animation_manager2 = AnimationManager(widget=button2)
def resizeEvent(self, event):
super().resizeEvent(event)
self.animation_manager1.reset()
self.animation_manager2.reset()
How does one use a QFileSystemModel to populate several QComboBox with subdirectories?
I have built a project management tool that allows me to create and manage my projects. I am currently using a combination of os.listdir and json to populate and validate my QComboboxes. But I am trying to learn a more modelview approach with QFileSystemModel.
So this is what I have:
class FileSystemModel(QW.QFileSystemModel):
def __init__(self, root, parent=None):
QW.QFileSystemModel.__init__(self, parent)
self.root = root
self.rootIndex = self.setRootPath(root)
class Window(QW.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__()
self.init()
def init(self):
layout = QW.QVBoxLayout()
self.cbox = QW.QComboBox()
self.cbox2 = QW.QComboBox()
self.model = FileSystemModel("C:\\projects\\")
self.cbox.setModel(self.model)
self.cbox2.setModel(self.model)
self.cbox.setRootModelIndex(self.model.rootIndex)
self.cbox.currentIndexChanged.connect(self._indexChanged)
layout.addWidget(self.cbox)
layout.addWidget(self.cbox2)
self.setLayout(layout)
def _indexChanged(self):
row = self.sender().currentIndex()
index = self.sender().rootModelIndex().child(row, 0)
self.cbox2.setRootModelIndex(index)
def main():
app = QW.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
I was attempting to repopulate the cbox2 using the index from cbox, but with my code it doesn't seem to work - it just stays empty.
Okay here is modified version of what you had:
from sys import exit as sysExit
from PyQt5.QtCore import QDir, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QFileSystemModel, QHBoxLayout, QComboBox
class SysDirModel(QFileSystemModel):
def __init__(self, DirPath):
QFileSystemModel.__init__(self)
self.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs)
self.setReadOnly(True)
# Property
self.setRootPath(DirPath)
# Property
self.RootIndex = self.index(DirPath)
class SysFileModel(QFileSystemModel):
def __init__(self, DirPath):
QFileSystemModel.__init__(self)
self.setFilter(QDir.NoDotAndDotDot | QDir.Files)
self.setReadOnly(True)
# Property
self.setRootPath(DirPath)
# Property
self.RootIndex = self.index(DirPath)
def ResetPath(self, DirPath):
self.setRootPath(DirPath)
self.RootIndex = self.index(DirPath)
class MainWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.setGeometry(150, 150, 450, 100)
# If you use forward slash this works in Windows as well and it is cleaner
self.SysDirs = SysDirModel('C:/projects/')
self.SysFils = SysFileModel('C:/projects/')
# Setup first ComboBox
self.cbxDirs = QComboBox()
self.cbxDirs.setMinimumWidth(200)
self.cbxDirs.setModel(self.SysDirs)
self.cbxDirs.setRootModelIndex(self.SysDirs.RootIndex)
# This sends a Signal to a predefined Slot
self.cbxDirs.currentIndexChanged.connect(self.IndexChanged)
self.cbxFiles = QComboBox()
self.cbxFiles.setMinimumWidth(200)
self.cbxFiles.setModel(self.SysFils)
self.cbxFiles.setRootModelIndex(self.SysFils.RootIndex)
HBox = QHBoxLayout()
HBox.addWidget(self.cbxDirs)
HBox.addStretch(1)
HBox.addWidget(self.cbxFiles)
self.setLayout(HBox)
# This is the receiver of a Signal (aka Slot) so it ought to be used as such
#pyqtSlot(int)
def IndexChanged(self, RowIdx):
# Get your Current DirPath based on the Selected Value
index = self.cbxDirs.rootModelIndex().child(RowIdx, 0)
DirPath = self.cbxDirs.model().filePath(index)
# Reset what ComboBox 2's Model and what it is looking at
self.cbxFiles.clear()
self.SysFils.ResetPath(DirPath)
self.cbxFiles.setModel(self.SysFils)
if __name__ == '__main__':
MainThred = QApplication([])
MainGui = MainWindow()
MainGui.show()
sysExit(MainThred.exec_())
I'm using QFrame to make 'cards' with drop shadows using QGraphicsDropShadowEffect. The issue I'm having, is if I add a button to the card, then add a drop shadow to the button, the button is invisible but still clickable. If I remove the drop shadow from the card, it shows the button fine, or if I remove the shadow from the button, it's visible. It seems I can't have drop shadows on both the card and the button.
My classes:
class Card(QFrame):
""" """
def __init__(self, title='Card Title', cls_layout=QVBoxLayout,
vsizing=QSizePolicy.Preferred, hsizing=QSizePolicy.Preferred,
has_shadow=False, subtitle='', parent=None):
super().__init__(parent)
self.has_shadow = has_shadow
self.setStyleSheet('QFrame { border-radius: 2px; background: white;}')
self._layout = QVBoxLayout(self)
self._layout.setAlignment(Qt.AlignTop)
self._lblTitle = QLabel(title)
self._lblTitle.setFont(make_font(pt=12, b=True))
self._lblSubtitle = QLabel()
self._lblSubtitle.setIndent(4)
self._layout.addWidget(self._lblTitle)
self._layout.addWidget(self._lblSubtitle)
self._layout.addSpacing(4)
#self.subtitle = subtitle # property not used for example
self.enabled = True
self.layout = cls_layout() # where content is added
self._layout.addLayout(self.layout)
self.setSizePolicy(hsizing, vsizing)
#property
def enabled(self):
return self.isEnabled()
#enabled.setter
def enabled(self, state):
if state and self.has_shadow:
effect = QGraphicsDropShadowEffect(self)
effect.setOffset(1, 2)
effect.setBlurRadius(4)
self.setGraphicsEffect(effect)
else:
self.setGraphicsEffect(None)
self.setEnabled(state)
class PushButton(QPushButton):
def __init__(self, text, width=75, height=30, parent=None):
super().__init__(text, parent)
self.setFixedWidth(width)
self.setFixedHeight(height)
self.enabled = True
#property
def enabled(self):
return self.isEnabled()
#enabled.setter
def enabled(self, state):
self.setGraphicsEffect(None)
if state:
self.effect = QGraphicsDropShadowEffect(self)
self.effect.setOffset(1, 2)
self.effect.setBlurRadius(4)
self.setGraphicsEffect(self.effect)
self.setEnabled(state)
This is an example of how I'm using them:
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
import sys
if __name__ == '__main__':
app = QApplication([])
window = QMainWindow()
widget = QWidget()
layout = QVBoxLayout(widget)
window.setCentralWidget(widget)
card = Card(has_shadow=True)
btn = PushButton('Test Button')
btn.clicked.connect(print)
card.layout.addWidget(btn)
btn.enabled = False # Button Visible
# btn.enabled = True # Button Invisible, but still click-able
layout.addWidget(card)
window.show()
sys.exit(app.exec_())
I tried ditching the drop shadow on the card and setting the QFrame to StyledPanel with Raised shadow, but I can't get them to show up, even if I remove the css from it. I'm using Windows 7 with Anaconda 4.4 if that makes a difference. I'm assuming I'm either using the graphics effect wrong or doing something else incorrectly, but I haven't been able to find any other posts with a similar issue.
I ended up using this method and changing my button class to:
class Button(QWidget):
clicked = pyqtSignal()
def __init__(self, text, w=75, h=50, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(QMargins(0, 0, 0, 0))
self.setFixedSize(w + 5, h + 6)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self)
self.view.setScene(self.scene)
self._btn = QPushButton(text)
self._btn.setFixedSize(w, h)
self._btn.setAttribute(Qt.WA_TranslucentBackground)
self._btn.setStyleSheet(gui.css.button_css())
self._btn.clicked.connect(self.click)
self.btn = self.scene.addWidget(self._btn)
self.enabled = True
layout.addWidget(self.view)
#property
def enabled(self):
return self._btn.isEnabled()
#enabled.setter
def enabled(self, state):
self._btn.setEnabled(state)
if state:
shadow = QGraphicsDropShadowEffect(self)
shadow.setOffset(1, 2)
shadow.setBlurRadius(4)
self.btn.setGraphicsEffect(shadow)
else:
self.btn.setGraphicsEffect(None)
def click(self):
self.clicked.emit()