How to control the behaviour of QGridLayout - pyqt

Using PyQT6
I am trying to create and app that adds a new frame (stack1) whenever the check box is clicked. It all works well until the app hides the stack when you untick the checkbox.
Clearly what I'd like is for the app to return to the initial shape when I hide the stack. I am aware that since I have added a column to the QTGrid,the window has likely reshaped taking into account the extra space. I am getting the same behaviour using deleteLater. So my question is how can I avoid that? Is there a way to reset the gridLayout to go back to just 1 column?
Here is a reproducible example:
import sys
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from PyQt6.QtWidgets import QWidget, QApplication, QVBoxLayout, QStackedWidget,
QLabel, QPushButton
import sys
from PyQt6.QtGui import QIcon
from PyQt6 import QtGui
class Window(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Anticoagulant Dose calculator")
self.buttons = {'metrics': ['Age?', 'Height in cm?', 'Serum Creatinine?', 'Weight in kg?', 'Sex?'],
'anticoagulant': ['DOAC', 'warfarin, \nacenocumarol'],
'indication': ['VTE', 'Atrial Fibrilation', 'Mechanical Heart valve'],
'VTE': ['single VTE > 12 months', 'VTE 3-12 months or \n multiple, large volume PE or \n active cancer', 'VTE < 3 months \n or known antithrombin deficiency or \n antiphospholipid syndrome'],
'af': ['No',"Yes, within the past 12 months","Yes, within the past 3 months"],
'Mech': 'mech',
'sexo': ['Male', 'Female']}
self.dialogLayout = QVBoxLayout()
self.anticoagLayout = QVBoxLayout()
self.rejillaLayout = QGridLayout()
self.anticoagButtongroup = QButtonGroup()
self.indicationButtongroup = QButtonGroup()
self.UIcomponents()
self.setLayout(self.rejillaLayout)
self.stack1 = QWidget()
self.stack1UI()
def stack1UI(self):
for a in self.buttons['anticoagulant']:
self.anticoag_rb = QRadioButton(a)
self.anticoagButtongroup.addButton(self.anticoag_rb)
self.dialogLayout.addWidget(self.anticoag_rb)
for i in self.buttons['indication']:
self.indication_rb = QRadioButton(i)
self.indicationButtongroup.addButton(self.indication_rb)
self.dialogLayout.addWidget(self.indication_rb)
self.stack1.setLayout(self.dialogLayout)
def cb_onclicked(self):
self.cbox = self.bridgingCheckBox.sender()
try:
if self.cbox.isChecked():
print('Puenteando')
self.rejillaLayout.addWidget(self.stack1, 0, 2)
self.stack1.show()
else:
self.stack1.deleteLater()
print('No puenteando')
except RuntimeError:
pass
def UIcomponents(self):
self.entries = {}
formas = QFormLayout()
for m in self.buttons['metrics']:
if m != 'Sex?':
self.ent = QLineEdit()
formas.addRow(m, self.ent)
else:
print('Sexo?')
self.sComboBox = QComboBox()
self.sComboBox.addItems(self.buttons['sexo'])
sLabel = QLabel('Sex:?')
sLabel.setBuddy(self.sComboBox)
formas.addRow(sLabel, self.sComboBox)
self.entries[m] = self.ent
self.bridgingCheckBox = QCheckBox("Bridging dose")
self.bridgingCheckBox.toggled.connect(self.cb_onclicked)
formas.addRow(self.bridgingCheckBox, None)
self.rejillaLayout.addLayout(formas, 0, 0)
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.setGeometry(50, 50, 300, 160)
window.show()
sys.exit(app.exec())
Many thanks in advance.

Thanks yo #ekhumoro for the answer:
gridding the stack at the init stage then hiding it, and then adding the code to the checkbox connector does it.
def cb_onclicked(self):
self.cbox = self.bridgingCheckBox.sender()
self.stack1.setVisible(self.bridgingCheckBox.isChecked())
self.adjustSize()

Related

PyQt6 Multithreading thousands of tasks with progress bar

Please bear with me I am just getting into PyQt6 multithreading so this question might seem too simple and very well might have been answered previously, however I am at an impass and none of the answers or posts about the topic really make anything clear to me. I am looking to be able to click a button it then starts a thread for each task and as each one finishes it updates the progress bar. Here is what I have so far:
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton,QVBoxLayout, QWidget, QProgressBar
import sys
from time import sleep
from random import random
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.fi = [1,2,3,4,5,6,7,8,9,10] * 100
self.p = None
# add the widgets
self.btn = QPushButton("Execute")
self.btn.pressed.connect(self.start_threads)
self.progress = QProgressBar()
self.progress.setRange(0, len(self.fi))
l = QVBoxLayout()
l.addWidget(self.btn)
l.addWidget(self.progress)
w = QWidget()
w.setLayout(l)
self.setCentralWidget(w)
def task(self,num):
sleep(random())
# where i will run the task using what is passed through
def start_threads(self):
with ThreadPoolExecutor(len(self.fi)) as executor:
futures = []
for i in self.fi:
futures.append(executor.submit(lambda:self.task(i)))
for future in as_completed(futures):
value = self.progress.value()
self.progress.setValue(value + 1)
print(value)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Whilst this does nearly all my criteria the it only runs 998 of the 1000 tasks it is supposed to. It also makes the GUI freeze which is exactly what I'm trying to avoid by multithreading. I am aware that Qt workers exist however my test for them did not result in any success. I am also aware that this is definetly not how this should be done and thats why I am asking for a more acceptable solution.

Retrieve the path of selected files in a QTreeView

I would like to select files in a QTreeView and in the end I would like to get the path of the selected files, what should I do?
Here is a small example that I partly found on stackoverflow that I transformed a bit:
from PyQt5.QtCore import QDir, Qt
from PyQt5.QtWidgets import QFileSystemModel, QTreeView, QAbstractItemView, QApplication, QFileIconProvider
from PyQt5.QtGui import QIcon
import sys, os
from chemin_data_ressources import chemin_ressources
class ArbreArborescence(QTreeView):
def __init__(self, parent=None):
super(ArbreArborescence, self).__init__(parent)
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragOnly)
self.setDefaultDropAction(Qt.CopyAction)
self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setStyleSheet("background-color: rgb(180, 180, 180);alternate-background-color: rgb(180, 180, 180);")
def set_source(self, folder):
""" """
self.up_folder = os.path.dirname(folder)
self.dirModel = QFileSystemModel()
self.dirModel.setRootPath(QDir.homePath()) # Sous windows QDir.Drives() éventuellement
self.setModel(self.dirModel)
self.setRootIndex(self.dirModel.index(self.up_folder)) #
self.setWordWrap(True)
self.hideColumn(1)
self.hideColumn(2)
self.hideColumn(3)
idx = self.dirModel.index(folder)
self.expand(idx)
self.scrollTo(idx, QAbstractItemView.EnsureVisible)
self.dirModel.setNameFilters(["*.jpg", "*.jpeg", "*.png", "*.gif"])
self.dirModel.setNameFilterDisables(False)
self.dirModel.setIconProvider(IconProvider())
class IconProvider(QFileIconProvider):
def icon(self, fileInfo):
if fileInfo.isDir():
return QIcon(chemin_ressources(os.path.join('ressources_murexpo', 'icone_png_dossier_apsc_256x256.png')))
return QFileIconProvider.icon(self, fileInfo)
if __name__=='__main__':
app = QApplication(sys.argv)
w = ArbreArborescence()
w.set_source(os.path.expanduser('~')) # Fonctionne ! (ou '/home')
w.show()
app.exec_()
Help me please.
Use QFileSystemModel.filePath to get the path of an item at a given QModelIndex.
class ArbreArborescence(QTreeView):
...
def currentChanged(self, current, previous):
print(self.model().filePath(current))
super().currentChanged(current, previous)

Making a flashing text in tinker

I want a text that flashes with a clock's seconds. This Link was helpful, but couldn't solve my problem. Below is my little working code:
from tkinter import *
from datetime import datetime
import datetime as dt
import time
def change_color():
curtime=''
newtime = time.strftime('%H:%M:%S')
if newtime != curtime:
curtime = dt.date.today().strftime("%B")[:3]+", "+dt.datetime.now().strftime("%d")+"\n"+newtime
clock.config(text=curtime)
clock.after(200, change_color)
flash_colours=('black', 'red')
for i in range(0, len(flash_colours)):
print("{0}".format(flash_colours[i]))
flashing_text.config(foreground="{0}".format(flash_colours[i]))
root = Tk()
clock = Label(root, text="clock")
clock.pack()
flashing_text = Label(root, text="Flashing text")
flashing_text.pack()
change_color()
root.mainloop()
This line of code: print("{0}".format(flash_colours[i])) prints the alternating colors on the console as the function calls itself every 200s. But the flashing_text Label's text foreground doesn't change colors.
Does anybody have a solution to this problem? Thanks!
Please forgive my bad coding.
Although you have changed the color of the flashing_text in the for loop twice, but the tkinter event handler (mainloop()) can only process the changes when it takes back the control after change_color() completed. So you can only see the flashing_text in red (the last color change).
To achieve the goal, you need to change the color once in the change_color(). Below is a modified change_color():
def change_color(color_idx=0, pasttime=None):
newtime = time.strftime('%H:%M:%S')
if newtime != pasttime:
curtime = dt.date.today().strftime("%B")[:3]+", "+dt.datetime.now().strftime("%d")+"\n"+newtime
clock.config(text=curtime)
flash_colors = ('black', 'red')
flashing_text.config(foreground=flash_colors[color_idx])
clock.after(200, change_color, 1-color_idx, newtime)
I would add it to a class so you can share your variables from each callback.
So something like this.
from tkinter import *
from datetime import datetime
import datetime as dt
import time
class Clock:
def __init__(self, colors):
self.root = Tk()
self.clock = Label(self.root, text="clock")
self.clock.pack()
self.flashing_text = Label(self.root, text="Flashing text")
self.flashing_text.pack()
self.curtime = time.strftime('%H:%M:%S')
self.flash_colours = colors
self.current_colour = 0
self.change_color()
self.root.mainloop()
def change_color(self):
self.newtime = time.strftime('%H:%M:%S')
if self.newtime != self.curtime:
if not self.current_colour:
self.current_colour = 1
else:
self.current_colour = 0
self.curtime = time.strftime('%H:%M:%S')
self.flashing_text.config(foreground="{0}".format(self.flash_colours[self.current_colour]))
self.clock.config(text=self.curtime)
self.clock.after(200, self.change_color)
if __name__ == '__main__':
clock = Clock(('black', 'red'))

How do I sort a QStandardItemModel and QListView by checked objects?

I want to sort my ListView Model by checked objects. In the example below I've tried to add a self.model.sort(), but it doesn't seem to have any effect except for Qt.AscendingOrder, which was recently revealed to me.
The documentation says "You can search for items in the model with findItems(), and sort the model by calling sort()", but only self.model.sort(QtCore.Qt.DescendingOrder) seems to actually have an effect.
Do I misunderstand?
The GUI below set to launch whatever code is in the print_checked_items() after a set amount of time.
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QWizardPage, QListView
class AppRemovalPage(QWizardPage):
def __init__( self, parent ):
super(AppRemovalPage, self).__init__(parent)
self.list_view = QListView(self)
self.model = QtGui.QStandardItemModel(self.list_view)
for line in ('a', 'b', 'c', 'd', 'e'):
self.item = QtGui.QStandardItem(line)
self.item.setCheckable(True)
self.item.setCheckState(QtCore.Qt.Unchecked)
self.model.appendRow(self.item)
self.list_view.setModel(self.model)
self.list_view.clicked.connect(self.getSelectedListItem)
self.list_view.show()
def print_checked_items(self):
self.model.sort(QtCore.Qt.AscendingOrder) # has no effect
for index in range(self.model.rowCount()):
item = self.model.item(index)
if item.checkState() == QtCore.Qt.Checked:
print(item.text(), "was checked")
print("rerun timed script to list checked objects again")
def getSelectedListItem(self):
currentSelection = self.list_view.selectedIndexes()
name = None
row = None
index = None
# Set name for currently selected object in listview
for obj_index in currentSelection:
item = self.model.itemFromIndex(obj_index)
row = item.row()
index = self.model.index(row, 0)
name = self.model.data(index)
self.currName = name
self.currRow = row
self.currIndx = index
app = QApplication([])
listview = AppRemovalPage(None)
listview.show()
QTimer.singleShot(4000, listview.print_checked_items)
app.exec_()

Drag and drop in QTreeView fails with items that hold a QImage

I have a list of items in a QTreeView. Each item holds a QImage object. If I try to drag and drop the item, the program freezes. But when I comment out the line objMod._Image = QImage(flags = Qt.AutoColor), the program runs fine.
How can I drag and drop the items with the QImage object? The QImage holds an image which is rendered. The rendering process takes a while, so it would be nice to keep the QImage object.
import sys
import os
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtUiTools import *
from PIL import Image, ImageCms, ImageQt
class ObjModel:
def __init__(self):
self._Image = None
class DragMoveTest(QMainWindow):
def __init__(self):
super(DragMoveTest,self).__init__()
self.initGUI()
self.show()
def initGUI(self):
self.treeView = QTreeView()
modelTreeView = QStandardItemModel()
self.treeView.setModel(modelTreeView)
for i in range(0, 4):
objMod = ObjModel()
objMod._Image = None
objMod._Image = QImage(flags = Qt.AutoColor)
item = QStandardItem('Test: %s' % str(i))
item.setData(objMod, Qt.UserRole + 1)
modelTreeView.invisibleRootItem().appendRow(item)
self.treeView.setDragDropMode(QAbstractItemView.InternalMove)
self.setCentralWidget(self.treeView)
def main(args):
app = QApplication(sys.argv)
qt_main_wnd = DragMoveTest()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main(sys.argv)
This is caused by a bug in PySide. During a drag and drop operation, the data in the dragged item must be serialized. This will be handled by Qt for most data-types, but for types that are specific to Python, special handling is required. This special handling seems to be broken in PySide. If your example is converted to PyQt, a TypeError is raised when trying to drag items, but the program does not freeze.
The source of the problem is that you are storing data using a custom Python class. PyQt uses pickle to serialize custom data-types, but it is not possible to also pickle the QImage that is stored in its __dict__, so the operation fails. I assume PySide must attempt something similar, but for some reason it does not raise an error when it fails. Qt grabs the mouse whilst dragging, so if the operation fails abnormally, it won't be released again, and the program will appear to freeze.
The simplest way to fix this is to avoid using a custom class to hold the QImage, and instead store the image directly in the item:
image = QImage()
item = QStandardItem('Test: %s' % i)
item.setData(image, Qt.UserRole + 1)
To store more data items, you can either use a different data-role for each one, or use a dict to hold them all:
data = {'image': QImage(), 'title': 'foo', 'timestamp': 1756790}
item.setData(data, Qt.UserRole + 1)
However, if you do this, you must always use string keys in the dict, otherwise you will face the same problems as before. (Using string keys means the dict can be converted into a QMap, which Qt knows how to serialize).
(NB: if you want to know whether a Qt class can be serialized, check the docs to see whether it defines the datastream operators).
I come up with a different solution. It is easier to have a object that holds a io.BytesIO. Your store the ImageData into the bytesIO variable. Upon on your image library you can open the image from the bytesIO variable.
In the demo the class ObjModel can handle QImage and Image from Pillow/PIL. If you use the set methods the image object will be converted into a bytesIO.
In short here a working example:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
import io
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtUiTools import *
from PIL import Image, ImageCms, ImageQt
########################################################################
class ObjModel:
""""""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
self._ImageByteIO = None
#----------------------------------------------------------------------
def getObjByte(self):
""""""
return self._ImageByteIO
#----------------------------------------------------------------------
def getQImage(self):
""""""
try:
self._ImageByteIO.seek(0)
qImg = QImage.fromData(self._ImageByteIO.getvalue())
return qImg
except:
return None
#----------------------------------------------------------------------
def getPILImage(self):
""""""
try:
self._ImageByteIO.seek(0)
img = Image.open(tBytesIO)
return img
except:
return None
#----------------------------------------------------------------------
def setObjByte(self, fileName):
""""""
try:
tBytesIO = io.BytesIO()
f = open (fileName, 'rb')
tBytesIO.write(f.read())
f.close()
self._ImageByteIO = tBytesIO
except:
self._ImageByteIO = None
#----------------------------------------------------------------------
def setQImage(self, qImg):
""""""
try:
tBytesIO = io.BytesIO()
qByteArray = QByteArray()
qBuf = QBuffer(qByteArray)
qBuf.open(QIODevice.ReadWrite)
qImg.save(qBuf, 'PNG')
tBytesIO = io.BytesIO()
tBytesIO.write(qByteArray.data())
self._ImageByteIO = tBytesIO
except:
self._ImageByteIO = None
#----------------------------------------------------------------------
def setPILImage(self, pImg):
""""""
tBytesIO = io.BytesIO()
pImg.save(tBytesIO, 'png')
self._ImageByteIO = tBytesIO
#----------------------------------------------------------------------
class DragMoveTest(QMainWindow):
def __init__(self):
""""""
super(DragMoveTest,self).__init__()
self.initGUI()
self.show()
#----------------------------------------------------------------------
def initGUI(self):
""""""
self.treeView = QTreeView()
modelTreeView = QStandardItemModel()
self.treeView.setModel(modelTreeView)
for i in range(0, 4):
objMod = ObjModel()
objMod.setQImage(QImage(flags = Qt.AutoColor))
item = QStandardItem('Test: %s' % str(i))
item.setData(objMod, Qt.UserRole + 1)
modelTreeView.invisibleRootItem().appendRow(item)
self.treeView.setDragDropMode(QAbstractItemView.InternalMove)
self.setCentralWidget(self.treeView)
#----------------------------------------------------------------------
def main(args):
app = QApplication(sys.argv)
qt_main_wnd = DragMoveTest()
ret = app.exec_()
sys.exit(ret)
#----------------------------------------------------------------------
if __name__ == "__main__":
main(sys.argv)

Resources