Complex context-menu submenu - python

I have a Qt5 application mainly driven by context menu.
Right now I have the standard structure with menu(s), submenu(s) and actions.
I would like to add, in place of a submenu, a small dialog with a few input widgets, something like this:
Is there any (possibly simple) way to get this?
I know I can open a normal dialog from popup, but that is not what I mean.
I would like to have normal submenu behavior, with chance to go back to parent menu... if possible.
Note: I'm actually using PyQt5, but I think this is a more general Qt question.

Following #G.M. advice I was able to partially solve my problem.
My code current code looks like:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class ActionFont(QWidgetAction):
def __init__(self, parent: QWidget, target: QPlainTextEdit):
super(ActionFont, self).__init__(parent)
self.setIcon(QIcon("font-face.svg"))
self.setText("Face")
w = QFontComboBox()
w.currentFontChanged.connect(self.doit)
self.setDefaultWidget(w)
self.cursor = target.textCursor()
self.char_format = self.cursor.charFormat()
font = self.char_format.font()
w.setCurrentFont(font)
# self.triggered.connect(self.doit)
def doit(self, font):
self.char_format.setFont(font)
self.cursor.setCharFormat(self.char_format)
class ActionSize(QWidgetAction):
def __init__(self, parent: QWidget, target: QPlainTextEdit):
super(ActionSize, self).__init__(parent)
self.setIcon(QIcon("font-size.svg"))
self.setText("Size")
self.has_changed = False
w = QSpinBox()
self.setDefaultWidget(w)
self.cursor = target.textCursor()
self.char_format = self.cursor.charFormat()
font = self.char_format.font()
size = font.pointSize()
w.setRange(6, 100)
w.setValue(size)
w.valueChanged.connect(self.doit)
w.editingFinished.connect(self.quit)
def doit(self, size):
print(f'ActionSize.doit({size})')
self.char_format.setFontPointSize(size)
self.cursor.setCharFormat(self.char_format)
self.has_changed = True
def quit(self):
print(f'ActionSize.quit()')
if self.has_changed:
print(f'ActionSize.quit(quitting)')
class Window(QMainWindow):
def __init__(self, parent=None):
from lorem import text
super().__init__(parent)
self.text = QPlainTextEdit(self)
self.setCentralWidget(self.text)
self.text.setContextMenuPolicy(Qt.CustomContextMenu)
self.text.customContextMenuRequested.connect(self.context)
self.text.appendPlainText(text())
self.setGeometry(100, 100, 1030, 800)
self.setWindowTitle("Writer")
def context(self, pos):
m = QMenu(self)
w = QComboBox()
w.addItems(list('ABCDE'))
wa = QWidgetAction(self)
wa.setDefaultWidget(w)
m.addAction('Some action')
m.addAction(wa)
m.addAction('Some other action')
sub = QMenu(m)
sub.setTitle('Font')
sub.addAction(ActionFont(self, self.text))
sub.addAction(ActionSize(self, self.text))
m.addMenu(sub)
pos = self.mapToGlobal(pos)
m.move(pos)
m.show()
app = QApplication([])
w = Window()
w.show()
app.exec()
This works with a few limitations:
I have been able to add just a single widget using setDefaultWidget(), if I try to
add a fill QWidget or a container (e.g.: QFrame) nothing appears in menu.
I have therefore not been able to prepend an icon (or a QLabel) to the widget.
Widget does not behave like a menu item (it does not close when activated); I tried to overcome that as implemented in ActionSize, but looks rather kludgy and I'm unsure if it's the right way to go.
I will therefore not accept my own answer in hope someone can refine it enough to be generally useful.

Related

Open popup notification on the same monitor where the mainwindow is in python an pyqt5

In my QMainWindow i have a button which opens a new QDialog in the bottom right monitorcorner with a successmessage when i click it.
Now, if i move the QMainWindow to another monitor (i have 3 monitor) and click the button the successmessage popup appears in the monitor where the QMainWindow was opened. What i want is that the popup message appears in the monitor where my QMainWindow actually is. So if i move the QMainWindow to Monitor 1 and click the button, the successpopup should opens in monitor 1. If the QMainWindow is in monitor 2, the successpopup should open in monitor 2 an same for monitor 3.
with
screenNumber = QDesktopWidget().screenNumber(self)
i can read the screennumber where the mainwindow is. and this works fine. Evertime i click the button i read out the screennumber. But i don't found a way, to set the screennumber to my notification.
Any ideas?
Edit:
maybe it helps if i show my notify class
notes.py
from UIs.UI_notify import Ui_Notification
from PyQt5.QtWidgets import QDialog, QApplication, QDesktopWidget
from PyQt5 import QtCore
from PyQt5.QtCore import QRect, QPropertyAnimation, QTimer
import sys
class icon():
checked = "check-circle"
alert = "times-circle"
question = "question-circle"
class notify(QDialog, Ui_Notification):
def __init__(self, parent=None):
super(notify,self).__init__(parent)
self.setupUi(self)
self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
## Some helping stuff
############################################################
parent_sSize = QDesktopWidget().screenGeometry(parent)
parent_screenNumber = QDesktopWidget().screenNumber(parent)
sSize = QDesktopWidget().screenGeometry()
screenNumber = QDesktopWidget().screenNumber()
print("notification ScreenNumber = " + str(screenNumber))
print(sSize.width())
print(sSize.height())
print("Parents ScreenNumber = " + str(parent_screenNumber))
print(parent_sSize.width())
print(parent_sSize.height())
self.Note_Exit.clicked.connect(self.close)
## ScreenSize from parent
############################################################
self.hidedPos = QRect(parent_sSize.width()-self.width()-10,
parent_sSize.height()-self.height()+200,
self.frameGeometry().width(),
self.frameGeometry().height())
self.showPos = QRect(parent_sSize.width()-self.width()-10,
parent_sSize.height()-self.height()-50,
self.frameGeometry().width(),
self.frameGeometry().height())
def setNote(self, icon=icon.checked, headline="Headline", text="Text"):
self.icon = icon
self.headline = headline
self.text = text
self.noty_Label_Icon.setText(self.icon)
self.noty_Label_Headline.setText(self.headline)
self.noty_Label_Text.setText(self.text)
self.setGeometry(self.hidedPos)
self.anim = QPropertyAnimation(self,b"geometry")
self.anim.setDuration(700)
self.anim.setEasingCurve(QtCore.QEasingCurve.OutBack)
self.anim.setEndValue(self.showPos)
self.anim.start()
self.notyTimer = QTimer()
self.notyTimer.singleShot(4000,self.hideNote)
def hideNote(self):
self.anim = QPropertyAnimation(self,b"geometry")
self.anim.setDuration(700)
self.anim.setEasingCurve(QtCore.QEasingCurve.InOutBack)
self.anim.setEndValue(self.hidedPos)
self.anim.start()
self.anim.finished.connect(self.close)
if __name__ == "__main__":
notes = QApplication(sys.argv)
dialog = notify()
dialog.show()
sys.exit(notes.exec())
You cannot use the size of the widget during its construction, as at that moment it has a default size (640x480 for top level widgets, 100x30 for widgets created with a parent, including dialogs): the only reliable option is to use the sizeHint() or ensure that the layout has been properly activated with adjustSize().
Then, you don't need the screen to get the target position, as the parent geometry will suffice, but you do need it for the start position, otherwise the dialog will "pop up" at an arbitrary point below the window. Note that QDesktopWidget is considered obsolete, and you should use QScreen instead.
Finally, since you might want to reuse the notification, the start position should be set when the popup is actually being shown, not before. The same goes for the position when hiding (in case the notification could be moved).
class Notify(QDialog, Ui_Notification):
def __init__(self, parent):
super().__init__(parent)
self.setupUi(self)
self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.showAnim = QPropertyAnimation(self, b'geometry')
self.showAnim.setDuration(700)
self.showAnim.setEasingCurve(QtCore.QEasingCurve.OutBack)
self.hideAnim = QPropertyAnimation(self, b'geometry')
self.hideAnim.setDuration(700)
self.hideAnim.setEasingCurve(QtCore.QEasingCurve.InOutBack)
self.hideTimer = QTimer(self, singleShot=True)
self.hideTimer.setInterval(4000)
self.hideTimer.timeout.connect(self.hideNote)
self.showAnim.finished.connect(self.hideTimer.start)
self.hideAnim.finished.connect(self.close)
def setNote(self, icon=icon.checked, headline="Headline", text="Text"):
self.icon = icon
self.headline = headline
self.text = text
self.noty_Label_Icon.setText(self.icon)
self.noty_Label_Headline.setText(self.headline)
self.noty_Label_Text.setText(self.text)
self.adjustSize() # important!
endRect = self.rect()
center = self.parent().geometry().center()
endRect.moveCenter(center)
screen = QApplication.screenAt(center)
startRect = QRect(endRect)
startRect.moveTop(screen.geometry().bottom())
self.setGeometry(startRect)
self.showAnim.setStartValue(startRect)
self.showAnim.setEndValue(endRect)
self.showAnim.start()
self.show()
def hideNote(self):
rect = self.geometry()
self.hideAnim.setStartValue(rect)
screen = QApplication.screenAt(rect.center())
rect.moveTop(screen.geometry().bottom())
self.hideAnim.setEndValue(rect)
self.hideAnim.start()
if __name__ == "__main__":
notes = QApplication(sys.argv)
notes.setStyle('fusion')
w = QMainWindow()
b = QPushButton('click!')
w.setCentralWidget(b)
w.show()
notify = Notify(w)
b.clicked.connect(lambda: notify.setNote())
sys.exit(notes.exec())
Be aware that if you're going to create the popup every time, you should also set the WA_DeleteOnClose attribute in order to destroy it when closed, otherwise it will be kept in memory.
Note: QTimer.singleShot() is a static function, creating an instance of QTimer to use it is pointless, as that instance won't be used and a new QTimer would be created anyway.
Thank you.
In the meantime i found a solution which works for me.
Look a little dirty but works.
could you tell me, whats the difference between my code an yours?
Why should i use your code, except the fact that you are a better programmer ;-)
I set the WA_DeleteOnClose Attribute.
Good to know that. Thanks
notes.py
from UIs.UI_notify import Ui_Notification
from PyQt5.QtWidgets import QDialog, QApplication
from PyQt5 import QtCore
from PyQt5.QtCore import QRect, QPropertyAnimation, QTimer
import sys
class icon():
checked = "check-circle"
alert = "times-circle"
question = "question-circle"
clock = "clock"
class notify(QDialog, Ui_Notification):
def __init__(self, parent=None):
super(notify,self).__init__(parent)
self.setupUi(self)
self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
self.setWindowModality(QtCore.Qt.NonModal)
self.Note_Exit.clicked.connect(self.close)
self.parent_h = self.parent().geometry().height()
self.parent_w = self.parent().geometry().width()
self.parent_x = self.parent().geometry().x()
self.parent_y = self.parent().geometry().y()
self.dialog_w = self.width()
self.dialog_h = self.height()
self.setGeometry(self.parent_x+self.parent_w-self.dialog_w-10, self.parent_y+self.parent_h-self.dialog_h+120, self.dialog_w, self.dialog_h)
## ScreenSize from parent
############################################################
def setNote(self, icon=icon.checked, headline="Headline", text="Text"):
self.icon = icon
self.headline = headline
self.text = text
self.noty_Label_Icon.setText(self.icon)
self.noty_Label_Headline.setText(self.headline)
self.noty_Label_Text.setText(self.text)
self.anim = QPropertyAnimation(self,b"geometry")
self.anim.setDuration(700)
self.anim.setEasingCurve(QtCore.QEasingCurve.OutBack)
self.anim.setEndValue(QRect(self.parent_x+self.parent_w-self.dialog_w-10, self.parent_y+self.parent_h-self.dialog_h-20, self.dialog_w, self.dialog_h))
self.anim.start()
self.notyTimer = QTimer()
self.notyTimer.singleShot(4000,self.hideNote)
def hideNote(self):
self.anim = QPropertyAnimation(self,b"geometry")
self.anim.setDuration(700)
self.anim.setEasingCurve(QtCore.QEasingCurve.InOutBack)
self.anim.setEndValue(QRect(self.parent_x+self.parent_w-self.dialog_w-10, self.parent_y+self.parent_h-self.dialog_h+120, self.dialog_w, self.dialog_h))
self.anim.start()
self.anim.finished.connect(self.close)
if __name__ == "__main__":
notes = QApplication(sys.argv)
dialog = notify()
dialog.show()
sys.exit(notes.exec())

PyQt5 removing button

In the list_widget I have added a add button I also want to add a remove button which asks which item you wants to remove and remove the chosen item. I was trying it to do but I didn't had any idea to do so .Also, please explain the solution I am a beginner with pyqt5 or I'd like to say absolute beginner.
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication,QMainWindow,
QListWidget, QListWidgetItem
import sys
class MyWindow(QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.x = 200
self.y = 200
self.width = 500
self.length = 500
self.setGeometry(self.x, self.y, self.width,
self.length)
self.setWindowTitle("Stock managment")
self.iniTUI()
def iniTUI(self):
# buttons
self.b1 = QtWidgets.QPushButton(self)
self.b1.setText("+")
self.b1.move(450, 100)
self.b1.resize(50, 25)
self.b1.clicked.connect(self.take_inputs)
# This is the button I want to define.
self.btn_minus = QtWidgets.QPushButton(self)
self.btn_minus.setText("-")
self.btn_minus.move(0, 100)
self.btn_minus.resize(50, 25)
# list
self.list_widget = QListWidget(self)
self.list_widget.setGeometry(120, 100, 250, 300)
self.item1 = QListWidgetItem("A")
self.item2 = QListWidgetItem("B")
self.item3 = QListWidgetItem("C")
self.list_widget.addItem(self.item1)
self.list_widget.addItem(self.item2)
self.list_widget.addItem(self.item3)
self.list_widget.setCurrentItem(self.item2)
def take_inputs(self):
self.name, self.done1 =
QtWidgets.QInputDialog.getText(
self, 'Add Item to List', 'Enter The Item you want
in
the list:')
self.roll, self.done2 = QtWidgets.QInputDialog.getInt(
self, f'Quantity of {str(self.name)}', f'Enter
Quantity of {str(self.name)}:')
if self.done1 and self.done2:
self.item4 = QListWidgetItem(f"{str(self.name)}
Quantity{self.roll}")
self.list_widget.addItem(self.item4)
self.list_widget.setCurrentItem(self.item4)
def clicked(self):
self.label.setText("You clicked the button")
self.update()
def update(self):
self.label.adjustSize()
def clicked():
print("meow")
def window():
apk = QApplication(sys.argv)
win = MyWindow()
win.show()
sys.exit(apk.exec_())
window()
The core issue here is the lack of separation of the view and the data. This makes it very hard to reason about how to work with graphical elements. You will almost certainly want to follow the Model View Controller design paradigm https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
which offers a systematic way to handle this separation.
Once you do so, it immediately becomes very straight forward how to proceed with the question: You essentially just have a list, and you either want to add a thing to this list, or remove one based on a selection.
I include an example here which happens to use the built-in classes QStringListModel and QListView in Qt5, but it is simple to write your own more specialized widgets and models. They all just use a simple signal to emit to the view that it needs to refresh the active information.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class StuffViewer(QMainWindow):
def __init__(self, model):
super().__init__()
self.setWindowTitle("Stock managment")
# 1: Use layouts.
hbox = QHBoxLayout()
widget = QWidget()
widget.setLayout(hbox)
self.setCentralWidget(widget)
# 2: Don't needlessly store things in "self"
vbox = QVBoxLayout()
add = QPushButton("+")
add.clicked.connect(self.add_new_stuff)
vbox.addWidget(add)
sub = QPushButton("-")
sub.clicked.connect(self.remove_selected_stuff)
vbox.addWidget(sub)
vbox.addStretch(1)
hbox.addLayout(vbox)
# 3: Separate the view of the data from the data itself. Use Model-View-Controller design to achieve this.
self.model = model
self.stuffview = QListView()
self.stuffview.setModel(self.model)
hbox.addWidget(self.stuffview)
def add_new_stuff(self):
new_stuff, success = QInputDialog.getText(self, 'Add stuff', 'Enter new stuff you want')
if success:
self.stuff.setStringList(self.stuff.stringList() + [new_stuff])
def remove_selected_stuff(self):
index = self.stuffview.currentIndex()
all_stuff = self.stuff.stringList()
del all_stuff[index.column()]
self.stuff.setStringList(all_stuff)
def window():
apk = QApplication(sys.argv)
# Data is clearly separated:
# 4: Never enumerate variables! Use lists!
stuff = QStringListModel(["Foo", "Bar", "Baz"])
# The graphical components is just how you interface with the data with the user!
win = StuffViewer(stuff)
win.show()
sys.exit(apk.exec_())
window()

Qaction Shortcut in extended contextmenu not triggered

I try to extend the contextmenu of a QLineEdit with an additional entry for replacing text. I can extend the contextmenu with .createStandardContextMenu(), which works fine. But when I try to add a shortcut with .setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) it will not react on the key. Same with different keys, which I tried. In addition the shortcut made with QAction('&Replace', self) doesn't work too.
Some examples here in SO and other sources are constructed in the same way, so I'm wondering that nobody else has got the same problem. Seems that I'm missing anything. But what? I can't figure out, checked the docs multiple times.
Working Example:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
print("my_contextMenuEvent")
menu = self.lineEdit.createStandardContextMenu()
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
menu.addAction(action)
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()
Based on musicamante's comment I came to the following solution:
Extract from the docs:
If you want to extend the standard context menu, reimplement this
function, call createStandardContextMenu() and extend the menu
returned.
The default use of the QAction list (as returned by actions()) is to
create a context QMenu.
It's not totally logically to me, not for the 1st time ;-)
Final code:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
self.lineEdit.addAction(action)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
menu = self.lineEdit.createStandardContextMenu()
menu.addActions(self.lineEdit.actions())
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()

I want to know how to use pyqt5 to load the created widget at once

Sorry. I will modify the contents. I would like to load a widget inside def test by pressing Qbutton. Can not you use QStackedWidget to load the widget's configured functions? I've compiled the class and called it, but only a = QLineEdit ('Qline', self). I wonder what should be done to switch widgets.
You can also create a table like html using pyqt5.
import sys
from PyQt5.QtWidgets import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.stacked = QStackedWidget(self)
self.FirstpUI()
def FirstpUI(self):
self.btn1 = QPushButton('test1', self)
self.btn1.move(50,50)
self.btn1.clicked.connect(self.btn1_click)
def test(self):
a = QLineEdit('Qline', self)
b = QLineEdit('Qline2', self)
c = QPushButton('button', self)
a.move(0, 0)
b.move(100, 0)
c.move(50,50)
c.clicked.connect(self.btn2_click)
def btn1_click(self):
self.btn1.deleteLater()
self.stacked.addWidget(self.test())
self.stacked.setCurrentIndex(self.stacked.currentIndex()+1)
def btn2_click(self):
QMessageBox.about(self,'hello','hello2')
if __name__ == "__main__":
app = QApplication(sys.argv)
fream = MainWindow()
fream.show()
app.exec_()
May be I don't know what you real want,because I know that little, I think You can use QtDesigner,it's very useful

Displaying Gif Loading in PyQt4

I am trying to display a loading gif while other code executes. I am very unfamiliar with PyQt and have tried following the code at this link, which seems to be the standard way of executing a gif. I only want the gif playing and do not want a button. Here is the code I am currently at, but it is very poor.
self.movie = QMovie(coffeeloading.gif, self)
size = self.movie.scaledSize()
self.movie_screen.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.movie_screen.setAlignment(Qt.AlignCenter)
self.movie_screen = QLabel()
main_layout = QVBoxLayout()
main_layout.addWidget(self.movie_screen)
self.setLayout(main_layout)
self.movie.setCacheMode(QMovie.CacheAll)
self.movie.setSpeed(100)
self.movie_screen.setMovie(self.movie)
self.movie.start()
This is supposed to execute after a button press and fill up the whole screen of 240x320, but I dont have any idea how to do it. I have already read through many of the other stackoverflow and other links, but none of them seem to address how to complete this.
As an alternative, you can use this class here.
QtWaitingSpinner extends the QtWidgets.QWidget class and you can start your spinner by simply running
your_QtWaitingSpinnerObject.start()
You can check this answer as well.
# -*- coding: utf-8 -*-
import sys
from PySide.QtGui import *
from PySide.QtCore import *
class test_widget(QWidget):
m_play_state = False
def __init__(self):
super(test_widget, self).__init__()
self.__ui__()
def __ui__(self):
t_lay_parent = QVBoxLayout()
self.m_label_gif = QLabel()
self.m_button_play = QPushButton("Play")
t_lay_parent.addWidget(self.m_label_gif)
t_lay_parent.addWidget(self.m_button_play)
self.m_movie_gif = QMovie("loding.gif")
self.m_label_gif.setMovie(self.m_movie_gif)
self.m_label_gif.setScaledContents(True)
self.m_label_gif.hide()
self.setLayout(t_lay_parent)
self.m_button_play.clicked.connect(self.slt_play)
def slt_play(self):
if self.m_play_state:
self.m_label_gif.hide()
self.m_movie_gif.stop()
self.m_play_state = False
else:
self.m_label_gif.show()
self.m_movie_gif.start()
self.m_play_state = True
if __name__ == "__main__":
app = QApplication(sys.argv)
win = test_widget()
win.show()
sys.exit(app.exec_())

Categories