I am trying to create a button in a window that will ultimately run a function that I have written from code. My GUI code so far is as follows:
import sys
from PyQt4 import QtGui, QtCore
class PAINSFILTER(QtGui.QMainWindow):
def __init__(self):
QtGui.QWidget.__init__(self)
# Layout Init.
self.setGeometry (650, 300, 600, 600)
self.setWindowTitle('Data Viewer')
self.setWindowIcon(QtGui.QIcon('pill.png'))
extractAction = QtGui.QAction("Quit", self)
extractAction.setShortcut("Ctrl+Q")
extractAction.setStatusTip('Leave App')
extractAction.triggered.connect(self.close_application)
self.statusBar()
mainMenu = self.menuBar()
fileMenu = mainMenu.addMenu("&File")
fileMenu.addAction(extractAction)
self.uploadButton = QtGui.QPushButton('UPLOAD SDF', self)
self.runfilterButton = QtGui.QPushButton('Run Filter', self)
self.uploadButton.move (200, 50)
self.runfilterButton.move (400,50)
hBoxLayout = QtGui.QHBoxLayout()
hBoxLayout.addWidget(self.uploadButton)
hBoxLayout.addWidget(self.runfilterButton)
self.setLayout(hBoxLayout)
# Signal Init.
self.connect(self.uploadButton, QtCore.SIGNAL('clicked()'), self.open)
def open (self):
filename = QtGui.QFileDialog.getOpenFileName(self, 'Open File', '.')
print 'Path file:', filename
def close_application(self):
print ("Out")
sys.exit()
def run():
app = QtGui.QApplication(sys.argv)
mw = PAINSFILTER()
mw.show()
sys.exit(app.exec_())
run()
The function I am trying to run through the button is a simple nested loop that will take the file I uploaded through the "upload sdf" button and run it through that function. How do I link that function to a button using PyQt. Here is the function for reference:
suppl = Chem.SDMolSupplier('data.sdf') #this .sdf file is the one uploaded through the upload sdf button
for m in suppl:
for x in range(len(pains_smarts)):
if m.HasSubstructMatch(Chem.MolFromSmarts(pains_smarts[x])):
match+=1
has_pains.append(m)
ss.append(c)
else:
no_match+=1
c+=1
for m in suppl:
for x in range(len(pains_smarts)):
if m.HasSubstructMatch(Chem.MolFromSmarts(pains_smarts[x])):
pname.append(pains_name[x])
for m in suppl:
for x in range(len(pains_smarts)):
if m.HasSubstructMatch(Chem.MolFromSmarts(pains_smarts[x])):
psmarts.append(pains_smarts[x])
uniquess = set(ss)
w = Chem.SDWriter('pains_structures.sdf')
notpains = Chem.SDWriter('no_pains_structures.sdf')
temp = 0
for m in suppl:
if temp in uniquess:
w.write(m)
else:
notpains.write(m)
temp += 1
w.flush()
notpains.flush()
import csv
csv_out = open("pains_compounds.csv", "w")
mywriter = csv.writer(csv_out)
for row in zip(ss, psmarts, pname):
mywriter.writerow(row)
csv_out.close()
new_suppl = Chem.SDMolSupplier("pains_structures.sdf")
ms = [x for x in new_suppl if x is not None]
for m in ms: tmp = AllChem.Compute2DCoords(m)
from rdkit.Chem import Draw
img = Draw.MolsToGridImage(ms, molsPerRow = 6, subImgSize = (200, 200), legends = [x.GetProp("ID") for x in ms])
img.save("pains_img.png")
Take a look at the PyQt signaling functionality. For a QPushButton in particular it becomes:
buttonName.clicked.connect(functionName)
Whenever a button is clicked, it will emit the signal clicked which now has functionName registered to execute. n.b. you do not put parentheses after the functionName as you do not want the function to execute, but rather pass the function itself. Additionally the function needs to satisfy the output of the clicked signal, which emits a boolean (so functionName needs to take in that argument). In your case, since you want to pass another object, you can make a method:
def clickHandler(self,checked):
#call function with object to pass as argument here
which calls your method and takes in the object you want to pass
Related
The below code creates a simple window with a date-picker, and a button which activates the rest of the code (not the most elegant yet, and I'm using global variables here temporarily so the "processing" script can access them, these will be removed soon)
The problem is, when I create an object of the MyWindow class, I can't seem to access it at all in the functions called by start_processing().
import calendar
import os
from datetime import date
from glob import glob
from time import time
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QComboBox, QPushButton, QLabel, QErrorMessage
def start_processing():
start = time()
files_found: tuple = file_search(month, year, r'O:\test\1 Originals')
for i in files_found:
process_file(i)
duration = time() - start
window1.set_text(f'Generation of files took {duration:.1f} seconds!')
class MyWindow(object):
def __init__(self):
self.app = QApplication([])
self.progress = QLabel('Foo')
self.title = 'Bar'
self.left = 10
self.top = 10
self.width = 300
self.height = 300
self.error_dialog = QErrorMessage()
self.setup_ui()
def setup_ui(self):
self.app.setStyle('Fusion')
window = QWidget()
layout = QVBoxLayout()
combobox_month = QComboBox()
combobox_year = QComboBox()
layout.addWidget(self.progress)
layout.addWidget(combobox_month)
layout.addWidget(combobox_year)
combobox_month.addItems(calendar.month_name[1:13])
combobox_year.addItems(['2017', '2018', '2019', '2020'])
combobox_year.setToolTip('Select the year')
combobox_month.setToolTip('Select the month')
combobox_month.setCurrentText('January')
combobox_year.setCurrentText('2019')
process_button = QPushButton('Process')
layout.addWidget(process_button)
window.setLayout(layout)
combobox_month.currentTextChanged.connect(self.on_combobox_month_changed)
combobox_year.currentTextChanged.connect(self.on_combobox_year_changed)
process_button.clicked.connect(self.on_button_clicked)
window.setGeometry(self.left, self.top, self.width, self.height)
window.setWindowTitle(self.title)
window.show()
self.app.exec_()
#staticmethod
def on_button_clicked():
start_processing()
#staticmethod
def on_combobox_month_changed(text):
global month
month_lookup = {v: k for k, v in enumerate(calendar.month_name)}
month = month_lookup[text]
#staticmethod
def on_combobox_year_changed(text):
global year
year = int(text)
def set_text(self, text):
self.progress.setText(text)
self.app.processEvents()
def error_message(self, text):
self.error_dialog.showMessage(text)
if __name__ == '__main__':
window1 = MyWindow()
If I then try window1.set_text('Blah'), within the function start_processing() I get a NameError: name 'window1' is not defined.
start_processing is defined in the main body of the script, as are all functions except the ones included in this class.
Not sure how much of the entire script I need to post, but happy to add anything else.
The statement window1 = MyWindow() will not actually assign a value to the variable window1 until after MyWindow() has completely finished executing. Before MyWindow() can finish executing, __init__() must finish executing. But first setup_ui must finish executing. But first exec_ must finish executing. But exec_ blocks indefinitely as long as the window remains open. So when the user clicks on a button and triggers on_button_clicked, then window1 will not yet have a value.
One possible solution is to move the exec_ call so it is not called directly or indirectly by __init__(). For instance, you could call it at the file-level scope, right after creating MyWindow().
if __name__ == '__main__':
window1 = MyWindow()
window1.app.exec_()
I need to put 6 buttons on the window for my application. But as the application grows, more buttons might be needed (maybe 7, 8, ..). That is why I put those buttons in a Python list. Below you can see my code. You can simply copy/paste it, and it should run on your system without any errors:
import sys
import os
from PyQt4 import QtGui
from PyQt4 import QtCore
def setCustomSize(x, width, height):
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(x.sizePolicy().hasHeightForWidth())
x.setSizePolicy(sizePolicy)
x.setMinimumSize(QtCore.QSize(width, height))
x.setMaximumSize(QtCore.QSize(width, height))
''''''
class CustomMainWindow(QtGui.QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
# Define the geometry of the main window
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("my first window")
# Create FRAME_A
self.FRAME_A = QtGui.QFrame(self)
self.FRAME_A.setStyleSheet("QWidget { background-color: %s }" % QtGui.QColor(210,210,235,255).name())
self.LAYOUT_A = QtGui.QHBoxLayout()
self.LAYOUT_A.setAlignment(QtCore.Qt.AlignLeft)
self.FRAME_A.setLayout(self.LAYOUT_A)
self.setCentralWidget(self.FRAME_A)
# Place the buttons
self.placeButtons()
self.show()
''''''
def placeButtons(self):
self.btn = []
for i in range(6):
self.btn.append(QtGui.QPushButton(text = 'btn[' + str(i) + ']'))
setCustomSize(self.btn[i], 100, 50)
self.btn[i].clicked.connect(lambda: self.btnAction(i))
self.LAYOUT_A.addWidget(self.btn[i])
''''''
''''''
def btnAction(self,n):
print("btn[" + str(n) + "] is clicked")
''''''
''' End Class '''
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
''''''
I use the standard signal/slot mechanism to connect each button to the btnAction(self,n) function. As you can see, I use a special feature - lets call it the lambda feature - to pass on a parameter. The parameter is the button number.
Unfortunately, I get the following outputs, no matter what button I push:
>>> btn[5] is clicked
>>> btn[5] is clicked
>>> btn[5] is clicked
>>> btn[5] is clicked
I changed the placeButtons(self) function into:
def placeButtons(self):
self.btn = []
for i in range(6):
self.btn.append(QtGui.QPushButton(text = 'btn[' + str(i) + ']'))
setCustomSize(self.btn[i], 100, 50)
if(i==0):
self.btn[i].clicked.connect(lambda: self.btnAction(0))
elif(i==1):
self.btn[i].clicked.connect(lambda: self.btnAction(1))
elif(i==2):
self.btn[i].clicked.connect(lambda: self.btnAction(2))
elif(i==3):
self.btn[i].clicked.connect(lambda: self.btnAction(3))
elif(i==4):
self.btn[i].clicked.connect(lambda: self.btnAction(4))
elif(i==5):
self.btn[i].clicked.connect(lambda: self.btnAction(5))
self.LAYOUT_A.addWidget(self.btn[i])
''''''
''''''
Now I get the correct output:
>>> btn[0] is clicked
>>> btn[1] is clicked
>>> btn[2] is clicked
>>> btn[3] is clicked
So the new placeButtons(self) function kind of works, but it's just so inefficient and cumbersome. Does anyone know what was wrong with the first (and cleaner) implementation?
Note: I work on Windows 10, use Python v3 and make my GUI with the PyQt4 library (anaconda package).
EDIT
Thank you mr. salomonderossi. Now your solution works! I first have to import the functools:
import functools
Next I apply your solution:
def placeButtons(self):
self.btn = []
for i in range(6):
self.btn.append(QtGui.QPushButton(text = 'btn[' + str(i) + ']'))
setCustomSize(self.btn[i], 100, 50)
self.btn[i].clicked.connect(functools.partial(self.btnAction, i))
self.LAYOUT_A.addWidget(self.btn[i])
''''''
''''''
You need to bind the value for each created (lambda) function. To do that, you can pass them as parameter with a default value:
for i in range(6):
self.btn[i].clicked.connect(lambda i=i: self.btnAction(i))
Here is a minimal example to show the effect:
def test_function(x):
return x
def test():
# DO NOT USE THIS. This is just for showing the effect
wrong = list()
for i in range(6):
wrong.append(lambda: test_function(i))
for w in wrong:
print(w()) # will always print 5
print("-"*10)
# This is the desired behaviour
right = list()
for i in range(6):
right.append(lambda i=i: test_function(i))
for r in right:
print(r())
if __name__ == '__main__':
test()
Which gives the following output:
5
5
5
5
5
5
----------
0
1
2
3
4
5
Alternatively you can use partial from the functools package:
for i in range(6):
self.btn[i].clicked.connect(partial(self.btnAction, i))
I have a class to create a PyQt4 widget and another class to parse xml to get inputs. I want to create an UI dyanamically add buttons to the widget, reading from the xml(s) passed, which is not happening:
import sys
import xml.etree.ElementTree as ET
from PyQt4 import QtGui
ui = None
class userInterface(QtGui.QWidget):
def __init__(self):
super(userInterface, self).__init__()
def getFilesWindow(self):
self.filesSelected = []
fDialog = QtGui.QFileDialog.getOpenFileNames(self, 'Open file', '/Learning/Python/substance_XML_reading/', "SBS (*.sbs)")
for path in fDialog:
if path:self.filesSelected.append(path)
return self.filesSelected
class ParseXML():
def getXMLTags(self,fileList):
self.tags = []
if fileList:
print fileList
for eachFile in fileList:
fileToParse = ET.parse(eachFile)
root = fileToParse.getroot()
for child in root:
self.tags.append(child.tag)
return self.tags
def getSetUI(flist):
global ui
if flist:
tagsForBtns = ParseXML().getXMLTags(flist)
print tagsForBtns
for eachitem in tagsForBtns:
btn = QtGui.QPushButton(ui,eachitem)
def main():
app = QtGui.QApplication(sys.argv)
ui = userInterface()
fileListForUIGen = ui.getFilesWindow() # will return a list of files
getSetUI(fileListForUIGen) # Parses each file, gets tags, creates buttons and has to add to the current window..NOT WORKING
ui.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In order to add buttons to a widget, you need to place them inside a QLayout
class Widget(QtGui.QWidget):
def __init__(self):
super(Widget, self).__init__()
self.ui_lay = QtGui.QVBoxLayout()
self.setLayout(self.ui_lay)
def addButton(self, text):
btn = QtGui.QPushButton(text, self)
self.ui_lay.addWidget(btn)
...
for eachitem in tagsForBtns:
ui.addButton(eachitem)
Also, make sure to use global ui in your main() function if you're going to be doing it this way.
Personally, I don't see the reasons for splitting up all the function calls. I would just put them all in the UserInterface class.
In the example below, I would like to fill my QListWidget with files opening a Qdialog. I don't understand how I can add the files selected in the list. Should I do a new class? How can I connect the two methods setupList and addFiles?
import sys
from PyQt4 import QtCore, QtGui
from datapath import *
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.sources =[]
self.setupActions()
self.setupList()
self.setupUi()
self.setupStatusBars()
def addFiles(self):
files = QtGui.QFileDialog.getOpenFileNames(
self,"Open File", dirpath, "txt Files (*.txt)")
for string in files:
self.sources.append(str(string))
return self.sources
def setupActions(self):
self.exitAct = QtGui.QAction(
QtGui.QIcon(':/images/exit.png'),
"E&xit", self, shortcut="Ctrl+Q",
statusTip="Exit the application", triggered=self.close
)
self.addFilesAct = QtGui.QAction(
QtGui.QIcon(':/images/open.png'),
"Add &Files", self, shortcut=QtGui.QKeySequence.Open,
statusTip="Open an existing file", triggered=self.addFiles
)
def setupList(self):
#FileList
self.FileList = QtGui.QListWidget(self)
self.FileList.addItems(self.sources)
def setupUi(self):
#Window size
horiz = 300
vert = 300
self.setGeometry(self.width()/2, self.height()/2,horiz,vert)
self.setWindowTitle("test")
#MenuBar
self.FileMenu = self.menuBar().addMenu("&File")
self.FileMenu.addAction(self.addFilesAct)
self.FileMenu.addSeparator();
self.FileMenu.addAction(self.exitAct)
#ToolBar
self.fileToolBar = self.addToolBar("Open")
self.fileToolBar.addAction(self.addFilesAct)
self.fileToolBar.setIconSize(QtCore.QSize(64,64))
#Build Layout
mainLayout = QtGui.QVBoxLayout()
mainLayout.addWidget(self.FileList)
widget = QtGui.QWidget()
widget.setLayout(mainLayout)
self.setCentralWidget(widget)
def setupStatusBars(self):
self.statusBar().showMessage("Ready")
def main():
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When self.sources is changed, the elements of the QListWidget will not change. So self.FileList.addItems(self.sources) in setupList() should be removed and instead put in addFiles() so that every time the files are selected in the dialog, the QListWidget's addItems method is called. Then return self.sources in addFiles() would be unnecessary.
In order to append files to the listwidget, the addFiles method should look like this:
def addFiles(self):
files = QtGui.QFileDialog.getOpenFileNames(
self, "Open File", dirpath, "txt Files (*.txt)")
for string in files:
self.FileList.addItem(string)
The source list looks like it might be redundant. If you need to get the full list of files, you can do something like this:
sources = []
for row in range(self.FileList.count()):
item = self.FileList.item(row)
# python3
sources.append(item.text())
# python2, convert to python strings
# sources.append(unicode(item.text()))
print(sources)
I wrote this code, it's supposed to be a clipart browser (locked to "klip" folder under current directory).
When I start add signal at folder button (dbutton) I screw up: I don't know how to refresh the toolitemgroup content and icons keep appended, I can't remove manually since the button generated in loop.
And one more, how can I "auto-fit/trim" the button label? Cause it grows insane if the filename is lengthy.
import gtk
from os.path import join,normpath,splitext, isdir
from os import getcwd, walk, mkdir
klip="klip"
klipdir=join(getcwd(),klip)
#init dir
if not isdir(klipdir):
mkdir(klipdir)
class kaosmu:
def icon_builder_cb(self, widget, data=klip):
if data != klip:
homebutton = gtk.ToolButton(gtk.STOCK_HOME)
browser.add(homebutton)
upbutton = gtk.ToolButton(gtk.STOCK_GO_UP)
browser.add(upbutton)
#folder
idx=dirs.index(data)
print idx
for i in dirs[idx+1]:
dbutton = gtk.ToolButton(gtk.STOCK_OPEN)
dbutton.set_label(i)
dbutton.connect("clicked", self.icon_builder_cb, join(data,i))
browser.add(dbutton)
#files
for i in dirs[idx+2]:
pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(data,i),32,32)
img=gtk.Image()
img.set_from_pixbuf(pixbuf)
svg = gtk.ToolButton(img,splitext(i)[0])
browser.add(svg)
browser.show_all()
def im_browser_cb(self, widget, data=klipdir):
global dirs
dirs = []
for (p, d, f) in walk(data):
key=p[len(data)-len(klip):]
dirs.append(key)
dirs.append(d)
dirs.append(f)
def __init__(self):
self.im_browser_cb(None)
window = gtk.Window()
window.set_default_size(1024, 800)
window.set_border_width(2)
window.connect("destroy", lambda w: gtk.main_quit())
drawingarea = gtk.DrawingArea()
drawingarea.set_size_request(600, 700)
hpane = gtk.HPaned()
menuscroll = gtk.ScrolledWindow()
canvasscroll = gtk.ScrolledWindow()
toolpalette = gtk.ToolPalette()
toolpalette.set_style(gtk.TOOLBAR_BOTH)
toolpalette.set_icon_size(gtk.ICON_SIZE_DND)
hpane.set_position(250)
window.add(hpane)
hpane.add1(menuscroll)
hpane.add2(canvasscroll)
menuscroll.add_with_viewport(toolpalette)
canvasscroll.add_with_viewport(drawingarea)
global browser
browser = gtk.ToolItemGroup(" Browser ")
toolpalette.add(browser)
toolpalette.set_expand(browser, True)
self.icon_builder_cb(None)
window.show_all()
def main(self):
gtk.main()
if __name__ == "__main__":
kaos = kaosmu()
kaos.main()
I solved this with:
while browser.get_nth_item(0):
browser.remove(browser.get_nth_item(0))