I am building a parser with a main QWidget that takes a file input, and open a new QWidget and display a table with parsed data.
However, when I selected a new file from the main QWidget, it is replacing the second QWidget instead of instantiating another QWidget.
Also, I want to be able to close the main QWidget and close all QWidgets and exit the program. It is not doing that right now.
Full Code: main.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import QColor
from dbparser import DBParser
import xlsxwriter
class PopUp(QWidget):
def __init__(self, tablename, fields, rows):
super(PopUp, self).__init__()
#Window Property
self.title = tablename
self.left = 300
self.top = 250
self.width = 1000
self.height = 750
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
...
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'CMSDBParser'
self.left = 150
self.top = 150
self.width = 500
self.height = 200
self.file = ""
self.filepath = QLineEdit()
self.bigEditor = QTextEdit()
self.browse_btn = QPushButton('Browse...')
self.parse_btn = QPushButton('Parse...')
self.initUI()
...
def parse(self):
if self.file:
try:
parser = DBParser(self.file)
dbcheck = parser.isDB()
if dbcheck == True:
tablename, fields, rows = parser.getData()
self.popup = PopUp(tablename, fields, rows)
self.popup.show()
else:
QMessageBox.information(self, 'Warning', 'Right format but not CMS DB File')
except:
QMessageBox.critical(self, 'Error', "Invalid File Selected \nMust be .txt or .log format")
self.parse_btn.setEnabled(False)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The problem is that you are using the same attribute: self.popup and if you create another widget it will replace it eliminating the previous window, in this case the solution is to create a list that stores the PopUp, on the other hand it is recommended to enable the attribute Qt::WA_DeleteOnClose making that when the PopUp is closed it is eliminated from the memory so that no unnecessary memory will be kept. For the case of your second requirement, an option is to overwrite the closeEvent() method and close the PopUp, but to do this you must verify if it exists since when an item is deleted from memory it does not imply that you delete its reference in the list. use sip.isdeleted()
import sip
# others imports
class App(QWidget):
def __init__(self):
super().__init__()
# ...
self.popups = [] # <---
def parse(self):
if self.file:
try:
parser = DBParser(self.file)
if parser.isDB():
tablename, fields, rows = parser.getData()
popup = PopUp(tablename, fields, rows) # <---
popup.setAttribute(Qt.WA_DeleteOnClose) # <---
popup.show() # <---
self.popups.append(popup) # <---
else:
QMessageBox.information(self, 'Warning', 'Right format but not CMS DB File')
except:
QMessageBox.critical(self, 'Error', "Invalid File Selected \nMust be .txt or .log format")
self.parse_btn.setEnabled(False)
def closeEvent(self, event):
for popup in self.popups: # <---
if not sip.isdeleted(popup): # <---
w.close() # <---
super(App, self).closeEvent(event)
Related
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()
I have written a program in python with ROS. It contains a GUI for "robot teleoperation", and in its MainWindow I added 3 widgets (rviz, joystick, button panel). When I start MainWindow I get the following error:
raise rospy.exceptions.ROSException("rospy.init_node() has already been called with different arguments: "+str(_init_node_args))
rospy.exceptions.ROSException: rospy.init_node() has already been
called with different arguments: ('teleop_twist_keyboard',
['MainWindow.py'], False, None, False, False).
Joystick.py and Button.py contain ros.init_node() function. In MainWindow I instantiate Joystick and Button class and add them to MainWindow. I need to call ros.init_node() several times to communicate with various nodes.
directory structure
main window example
code main window
import sys
import PyQt5
import threading
from Joystick import Joystick
from QWidget_rviz import Rviz
from BaseArmPosition import BaseArmPosition
class MainWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self,*args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.title = 'Robot teleoperation'
self.left = 10
self.top = 10
self.width = 1920
self.height = 1080
self.joy = Joystick(maxDistance=50,MinimumSize=100,EclipseX=-20,EclipseY=40)
self.rviz = Rviz()
#self.arm_position = BaseArmPosition()
self.initUI()
def initUI(self):
self.central_widget = PyQt5.QtWidgets.QWidget()
self.setCentralWidget(self.central_widget)
grid = PyQt5.QtWidgets.QGridLayout(self.centralWidget())
grid.addWidget(self.joy, 0, 2)
grid.addWidget(self.rviz, 0, 0)
#grid.addWidget(self.arm_position, 0, 1)
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.show()
app = PyQt5.QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
code buttons widget, joistick.py has the same
import PyQt5
import rospy
from std_msgs.msg import String
class BaseArmPosition(PyQt5.QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super(BaseArmPosition, self).__init__(*args, **kwargs)
self.initUI()
def initUI(self):
self.pushButton_moveInit = PyQt5.QtWidgets.QPushButton("moveInit",self)
self.pushButton_moveLeft = PyQt5.QtWidgets.QPushButton("moveLeft",self)
self.pushButton_moveRight = PyQt5.QtWidgets.QPushButton("moveRight",self)
layout = PyQt5.QtWidgets.QVBoxLayout(self)
layout.addWidget(self.pushButton_moveRight)
layout.addWidget(self.pushButton_moveLeft)
layout.addWidget(self.pushButton_moveInit)
self.pushButton_moveInit.clicked.connect(self.moveInit)
self.pushButton_moveLeft.clicked.connect(self.moveLeft)
self.pushButton_moveRight.clicked.connect(self.moveRight)
rospy.init_node("controller_ur")
self.pub_arm = rospy.Publisher("/controller_ur/order",String,queue_size=1)
def moveInit(self):
moveInit = "moveInit"
msg = String()
msg.data = moveInit
self.pub_arm.publish(moveInit)
def moveLeft(self):
moveInit = "moveLeft"
msg = String()
msg.data = moveInit
self.pub_arm.publish(moveInit)
def moveRight(self):
moveInit = "moveRight"
msg = String()
msg.data = moveInit
self.pub_arm.publish(moveInit)
A ROS node can only be initialized once in your program. You should centralize the initialization of the node at the beginning of the main script, and the rest of imported modules should not try to initialize a new node, as that is not allowed.
If what you want is the different sub-modules to deal with different data, then you should just create separate topics within the same node.
Put this somewhere towards the top of your script before import statements.
import os
os.system("rosrun my_package_name my_node_script.py")
I created a widget, The process of widget begins with an entering the SQL.table_name and switch the Run_analysis button to generate the output in the format of csv.file. the mentioned process performed well. but I stuck in printing the (print) statement in the larger textbox.
class EpiClass:
NR = 0
R = 0
CT = 0
def __init__(self, obj):
self.obj = obj
def epi_calc(self):
"""Function to process EPI formula"""
with open(FN, "w") as FL: #FN object FL
for j in self.obj:
if j[12] is not None and float(j[12]) != 0: #Exclude (Null, 0) values in Cre-j[12]
j12 = float(j[12])
type(j12)
#type(j[12]) #tested
#type(j[35]) #tested
{
Body of statement, assume it add two variable
}
print("Total no of rows affected:", EpiClass.CT)
print("No. of records not analysed:", EpiClass.NR)
print("No. of records analysed:", EpiClass.R)
Here we go to PyQt5.
class WinClass(QMainWindow):
"""Main Window"""
def __init__(self):
super().__init__()
self.title = 'EGFR Widget'
self.left = 10
self.top = 10
self.width = 1920
self.height = 1080
self.init_ui()
def init_ui(self):
"""Window Geometry"""
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#label
# Create textbox
self.textbox = QLineEdit(self)
self.textbox.move(20, 20)
self.textbox.resize(280, 40)
# Create a button in the window
self.button = QPushButton('Run Analysis', self)
self.button.move(20, 80)
#Print affected Rows
self.textbox2 = QLineEdit(self)
self.textbox2.move(120, 120)
self.textbox2.resize(880, 140)
# connect button to function on_click
self.button.clicked.connect(self.on_click)
self.show()
#pyqtSlot()
def on_click(self):
"""Button Action function"""
tb_value = str(self.textbox.text())
new_class = Edb(tb_value)
new_class.eclass()
######Im messed up in this step to print that 3 statements in textbox2#############
tb2_value = str(self.textbox2.text(EpiClass.CT))
#tb3_value = str(self.textbox2.text(EpiClass.NR))
#tb4_value = str(self.textbox2.text(EpiClass.R))
if __name__ == '__main__':
APP = QApplication(sys.argv)
ex = WinClass()
sys.exit(APP.exec_())
Kindly suggest a code to resolve the print statement. Much thanks!
You're trying to write using a read-only method text, you should use setText instead. I would save those statements in some variable and access from your widget, but that's up to you. I hope it helps.
def on_click(self):
"""Button Action function"""
tb_value = "Total no of rows affected: {}".format(EpiClass.CT)
self.textbox2.setText(tb_value)
I have 2 tabs in PyQT5 created as separate classes. 1 class (tab) is to load excel file, second to set rules for removing unneeded rows. When I load file I need to post all headers to QComboBox in other tab so I can create rules. Can't make that to work.
import sys
from PyQt5.QtWidgets import *
import pandas as pd
class BOMAppWindow(QDialog):
def __init__(self):
super().__init__()
self.setGeometry(250, 150, 1400, 600)
mainTabWidget = QTabWidget() # --------- TABS UI ----------
mainTabWidget.addTab(FileOpenTab(), "File Open")
mainTabWidget.addTab(RowRemoveRulesTab(), "Row Delete Rules")
mainVbox = QVBoxLayout()
mainVbox.addWidget(mainTabWidget)
self.setLayout(mainVbox)
df = pd.DataFrame() # pandas dataframe as container for excel spreadsheet
header_list = []
class FileOpenTab(QDialog):
def __init__(self):
super().__init__()
vbox = QVBoxLayout()
h_top_box = QHBoxLayout()
excel_load_button = QPushButton("Open Excel File")
h_top_box.addWidget(excel_load_button) # ---- Adding Widgets to HBox
# ---------------- Adding Table Widget -----------
self.excel_table = QTableWidget()
vbox.addLayout(h_top_box) #-----Create Vertical Box Layout
vbox.addWidget(self.excel_table)
self.setLayout(vbox)
excel_load_button.clicked.connect(self.set_file_data)
# ----------- Open Excel Button Clicked Event
self.show()
def file_path(self): # ----------- file dialog box
filter_excel_only = "Excel Files (*.xlsx)"
filename = QFileDialog.getOpenFileName(self, 'Open Excel File', "", filter_excel_only)
return filename[0]
def set_file_data(self):
path_text = self.file_path()
if path_text:
global df
df = pd.read_excel(path_text, na_filter=False)
self.post_dataframe(df)
RowRemoveRulesTab().update_select_list()
def post_dataframe(self, dataframe):
column_count = dataframe.shape[1]
row_count = dataframe.shape[0]
self.excel_table.setColumnCount(column_count)
self.excel_table.setRowCount(row_count)
self.excel_table.setHorizontalHeaderLabels(list(dataframe.columns))
def header_list(self):
return list(df.columns)
class RowRemoveRulesTab(QDialog):
def __init__(self):
super().__init__()
hbox = QHBoxLayout()
groupBox1 = QGroupBox("Rule 1:")
vbox1 = QVBoxLayout()
self.rule1select = QComboBox()
self.rule1select.addItems(FileOpenTab().header_list())
vbox1.addWidget(self.rule1select)
groupBox1.setLayout(vbox1)
hbox.addWidget(groupBox1)
self.setLayout(hbox)
self.show()
def update_select_list(self):
self.rule1select.clear()
self.rule1select.addItems(FileOpenTab().header_list())
print(FileOpenTab().header_list())
app = QApplication(sys.argv)
bomAppWindow = BOMAppWindow()
bomAppWindow.show()
app.exec()
I need last function in second tab (or any other way to handle it) update_select_list(self): to update QComboBox with header list from excel file loaded in first FileOpenTab class. Now QComboBox remains blank after file load.
The key point of your problem is that you assume that every time you do FileOpenTab() it will always be the same widget but it is not. The same goes for RowRemoveRulesTab. Instead you have to store the object in a variable and reuse it.
On the other hand you have design problems, in this case you must use the Single responsibility principle where it indicates that each class has a function, and must provide methods for other objects to access the information, in Qt/PyQt the simplest way of transmitting information is through signals, in this case when the dataframe is loaded a signal will be emitted with the new headers, and that signal must be connected to a method of the other class so that it updates the information of the QComboBox.
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
class FileOpenTab(QtWidgets.QWidget):
headersChanged = QtCore.pyqtSignal(list)
def __init__(self, parent=None):
super(FileOpenTab, self).__init__(parent)
excel_load_button = QtWidgets.QPushButton(
"Open Excel File", clicked=self.load_file
)
self.excel_table = QtWidgets.QTableWidget()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(excel_load_button)
lay.addWidget(self.excel_table)
#QtCore.pyqtSlot()
def load_file(self):
filter_excel_only = "Excel Files (*.xlsx)"
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "Open Excel File", "", filter_excel_only
)
if filename:
df = pd.read_excel(filename, na_filter=False)
self.fill_table(df)
def fill_table(self, dataframe):
row_count, column_count = dataframe.shape
self.excel_table.setColumnCount(column_count)
self.excel_table.setRowCount(row_count)
headers = list(dataframe.columns)
self.excel_table.setHorizontalHeaderLabels(headers)
self.headersChanged.emit(headers)
class RowRemoveRulesTab(QtWidgets.QWidget):
def __init__(self, parent=None):
super(RowRemoveRulesTab, self).__init__(parent)
self.rule_select = QtWidgets.QComboBox()
group_box = QtWidgets.QGroupBox("Rule 1:")
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.rule_select)
group_box.setLayout(vbox)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(group_box)
#QtCore.pyqtSlot(list)
def update_items(self, items):
self.rule_select.clear()
self.rule_select.addItems(items)
class BOMAppWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(BOMAppWindow, self).__init__(parent)
file_open_tab = FileOpenTab()
row_remove_rules_tab = RowRemoveRulesTab()
file_open_tab.headersChanged.connect(row_remove_rules_tab.update_items)
mainTabWidget = QtWidgets.QTabWidget()
mainTabWidget.addTab(file_open_tab, "File Open")
mainTabWidget.addTab(row_remove_rules_tab, "Row Delete Rules")
self.setCentralWidget(mainTabWidget)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = BOMAppWindow()
w.show()
sys.exit(app.exec_())
I have a list which is generated based on user-input.
I am trying to display this list in a QMessageBox. But, I have no way of knowing the length of this list. The list could be long.
Thus, I need to add a scrollbar to the QMessageBox.
Interestingly, I looked everywhere, but I haven’t found any solutions for this.
Below is, what I hope to be a “Minimal, Complete and Verifiable Example”, of course without the user input; I just created a list as an example.
I appreciate any advice.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class W(QWidget):
def __init__(self):
super().__init__()
self.initUi()
def initUi(self):
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.lst = list(range(2000))
self.show()
def buttonClicked(self):
result = QMessageBox(self)
result.setText('%s' % self.lst)
result.exec_()
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())
You can not add a scrollbar directly since the widget in charge of displaying the text is a QLabel. The solution is to add a QScrollArea. The size may be inadequate so a stylesheet has to be used to set minimum values.
class ScrollMessageBox(QMessageBox):
def __init__(self, l, *args, **kwargs):
QMessageBox.__init__(self, *args, **kwargs)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
self.content = QWidget()
scroll.setWidget(self.content)
lay = QVBoxLayout(self.content)
for item in l:
lay.addWidget(QLabel(item, self))
self.layout().addWidget(scroll, 0, 0, 1, self.layout().columnCount())
self.setStyleSheet("QScrollArea{min-width:300 px; min-height: 400px}")
class W(QWidget):
def __init__(self):
super().__init__()
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.lst = [str(i) for i in range(2000)]
self.show()
def buttonClicked(self):
result = ScrollMessageBox(self.lst, None)
result.exec_()
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())
Output:
Here is another way to override the widgets behavior.
You can get references to the children of the widget by using 'children()'.
Then you can manipulate them like any other widget.
Here we add a QScrollArea and QLabel to the original widget's QGridLayout. We get the text from the original widget's label and copy it to our new label, finally we clear the text from the original label so it is not shown (because it is beside our new label).
Our new label is scrollable. We must set the minimum size of the scrollArea or it will be hard to read.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ScrollMessageBox(QMessageBox):
def __init__(self, *args, **kwargs):
QMessageBox.__init__(self, *args, **kwargs)
chldn = self.children()
scrll = QScrollArea(self)
scrll.setWidgetResizable(True)
grd = self.findChild(QGridLayout)
lbl = QLabel(chldn[1].text(), self)
lbl.setWordWrap(True)
scrll.setWidget(lbl)
scrll.setMinimumSize (400,200)
grd.addWidget(scrll,0,1)
chldn[1].setText('')
self.exec_()
class W(QWidget):
def __init__(self):
super(W,self).__init__()
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.message = ("""We have encountered an error.
The following information may be useful in troubleshooting:
1
2
3
4
5
6
7
8
9
10
Here is the bottom.
""")
self.show()
def buttonClicked(self):
result = ScrollMessageBox(QMessageBox.Critical,"Error!",self.message)
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())