I am practicing PyQt+Twisted by writing a chat client. This requires getting the two event loops to play nice. I want to figure out how to do this without the Twisted QTReactor. The pattern I'm trying to achieve is
Qt runs normally, Twisted runs in another thread (say a QThread).
Qt functions/methods call Twisted functions with callFromThread.
We hook up the deferreds given to us by Twisted so that they cause emission of Qt signals. In this way the Twisted thread can call back the Qt thread.
I've actually gotten that part working. The heart of the solution is this function
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback
func is the Twisted function you want to invoke. It probably returns a
deferred. successSignal is the signal you want to emit once the asynchronous
call finishes. This signal will be emitted with the result of func as an
argument
"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
The problem is that I don't know what to do when I get "un-asked-for" data. For example, someone else might send out a chat message. I need to be able to detect the receipt of the data in the Twisted thread and somehow get that data into the Qt thread. This seems tricky because in this case I haven't provided the Twisted part of the program with a signal to use as a callback. How can I do that?
Below please find complete example code to run my almost working chat client and server. To use these programs first copy the server and client as .py files and the ui specification as a .ui file. Run the server and then run the client. Once the client window is up, click the "connect" button. You should see a message on the server indicating a new connection. Then try typing into the line edit and clicking the "send" button. You'll see that the data makes it to the server and back, but that it doesn't show up in the client's QTextEdit box. This is because I can't figure out how to get the data from the ChatProtocol to the Qt part of the program. How can we do this?
SERVER (asyncore)
import asyncore
import socket
import constants as C
HOST = 'localhost'
PORT = 12344
class ChatServer(asyncore.dispatcher):
"""Receive and forward chat messages
When a new connection is made we spawn a dispatcher for that
connection.
"""
ADDRESS_FAMILY = socket.AF_INET
SOCKET_TYPE = socket.SOCK_STREAM
def __init__(self, host, port):
self.map = {}
self.address = (host,port)
self.clients = []
asyncore.dispatcher.__init__(self, map=self.map)
def serve(self):
"""Bind to socket and start asynchronous loop"""
self.create_socket(self.ADDRESS_FAMILY, self.SOCKET_TYPE)
self.bind(self.address)
print("ChatServer bound to %s %s"%self.address)
self.listen(1)
asyncore.loop(map=self.map)
def writable(self):
return False
def readable(self):
return True
def newMessage(self, data, fromWho):
"""Put data in all clients' buffers"""
print("new data: %s"%data)
for client in self.clients:
client.buffer = client.buffer + data
def handle_accept(self):
"""Deal with newly accepted connection"""
(connSock, clientAddress) = self.accept()
print("New connection accepted from %s %s"%clientAddress)
self.clients.append(ChatHandler(connSock, self.map, self))
class ChatHandler(asyncore.dispatcher):
def __init__(self, sock, map, server):
self.server = server
self.buffer = ''
asyncore.dispatcher.__init__(self, sock, map)
def writable(self):
return len(self.buffer) > 0
def readable(self):
return True
def handle_read(self):
"""Notify server of any new incoming data"""
data = self.recv(4096)
if data:
self.server.newMessage(data, self)
def handle_write(self):
"""send some amount of buffer"""
sent = self.send(self.buffer)
self.buffer = self.buffer[sent:]
if __name__=='__main__':
if HOST is None:
HOST = raw_input('Host: ')
if PORT is None:
PORT = int(raw_input('Port: '))
s = ChatServer(HOST, PORT)
s.serve()
CLIENT (PyQt + Twisted)
import sys
import PyQt4.QtGui as QtGui
import PyQt4.QtCore as QtCore
import PyQt4.uic as uic
import twisted.internet.reactor as reactor
import twisted.internet.defer as defer
import twisted.internet.protocol as protocol
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.networkThread = NetworkThread()
self.networkThread.start()
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
#Make connections from network thread signals to our slots
self.connect(self.networkThread,
self.networkThread.sigConnected,
self.onConnected)
self.connect(self.networkThread,
self.networkThread.sigNewData,
self.onNewData)
self.ui.show()
def getNetworkConnection(self):
factory = protocol.ClientCreator(reactor, ChatProtocol)
self.networkThread.callFromMain(factory.connectTCP,
self.networkThread.sigConnected,
'localhost', 12344)
def onConnected(self, p):
print("Got a protocol!")
self.cxn = p
def onNewData(self, data):
self.ui.outputBox.append('\r\n'+data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.networkThread.callFromMain(self.cxn.send, None, message)
class NetworkThread(QtCore.QThread):
"""Run the twisted reactor in its own thread"""
def __init__(self):
QtCore.QThread.__init__(self)
self.sigConnected = QtCore.SIGNAL("sigConnected")
self.sigNewData = QtCore.SIGNAL("sigNewData")
def run(self):
reactor.run(installSignalHandlers=0)
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
class ChatProtocol(protocol.Protocol):
def dataReceived(self, data):
print("Got data: %s"%data)
print("...but I don't know how to pass it to Qt :(")
def send(self, data):
self.transport.write(data)
class ChatFactory(protocol.ClientFactory):
protocol = ChatProtocol
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
UI file
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTextEdit" name="outputBox"/>
</item>
<item>
<widget class="QLineEdit" name="inputBox"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="text">
<string>connect</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
Related
I have the following test code:
from os import path
from PySide6.QtCore import QObject, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication
class MyWin(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
def show(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
with its associated MainWindow.ui:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>19</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
... which works as expected.
Question is: how do I replace the line:
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
with an equivalent using QMetaObject.connectSlotsByName(???) ?
Problem here is PySide6 QUiLoader is incapable to add widgets as children of self (as PyQt6 uic.loadUi(filename, self) can do) and thus I'm forced to put UI in a separate variable (self.ui) while slots are defined in "parent" MyWin.
How can I circumvent limitation?
Reason why I ask is my real program has zillions of signals/slots and connect()'ing them manually is a real PITA (and error-prone)
UPDATE:
Following advice I modified MyWin to inherit from QWidget, but enabling self.ui.setParent(self) is enough to prevent display of UI.
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
self.ui.setParent(self)
# QMetaObject.connectSlotsByName(self)
def myshow(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.myshow()
app.exec()
I also see some strange errors:
mcon#ikea:~/projects/pyside6-test$ venv/bin/python t.py
qt.pysideplugin: Environment variable PYSIDE_DESIGNER_PLUGINS is not set, bailing out.
qt.pysideplugin: No instance of QPyDesignerCustomWidgetCollection was found.
Qt WebEngine seems to be initialized from a plugin. Please set Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute and QSGRendererInterface::OpenGLRhi using QQuickWindow::setGraphicsApi before constructing QGuiApplication.
^C^C^C^C
Terminated
I need to kill process from another terminal, normal Ctrl-C is ignored.
UPDATE2:
I further updated code following #ekhumoro advice:
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget, QMainWindow
class UiLoader(QUiLoader):
_baseinstance = None
def createWidget(self, classname, parent=None, name=''):
if parent is None and self._baseinstance is not None:
widget = self._baseinstance
else:
widget = super(UiLoader, self).createWidget(classname, parent, name)
if self._baseinstance is not None:
setattr(self._baseinstance, name, widget)
return widget
def loadUi(self, uifile, baseinstance=None):
self._baseinstance = baseinstance
widget = self.load(uifile)
QMetaObject.connectSlotsByName(widget)
return widget
class MyWin(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
UiLoader().loadUi(path.join(path.dirname(__file__), "MainWindow.ui"), self)
# self.pushButton.clicked.connect(self.on_pushButton_clicked)
QMetaObject.connectSlotsByName(self)
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
This doesn't work either: it shows GUI, but button click is not connected (unless I explicitly do it uncommenting the line).
What am I doing wrong?
To answer the question as stated in the title:
It's possible to fix the original example by setting the container-widget as the parent of the ui-widget. However, there are a few extra steps required. Firstly, the flags of the ui-widget must include Qt.Window, otherwise it will just become the child of an invisble window. Secondly, the close-event of the ui-widget must be reimplemented so that the application shuts down properly. And finally, the auto-connected slots must be decorated with QtCore.Slot.
Here's a fully working example:
from os import path
from PySide6.QtCore import Qt, QEvent, Slot, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load(
path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.setParent(self, self.ui.windowFlags() | Qt.WindowType.Window)
self.ui.installEventFilter(self)
QMetaObject.connectSlotsByName(self)
def eventFilter(self, source, event):
if event.type() == QEvent.Type.Close and source is self.ui:
QApplication.instance().quit()
return super().eventFilter(source, event)
def myshow(self):
self.ui.show()
#Slot()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication(['Test'])
win = MyWin()
win.myshow()
app.exec()
PS: see also my completely revised alternative solution using a loadUi-style approach that now works properly with both PySide2 and PySide6.
connectSlotsByName() is a static function, and it can only accept an argument (the "target" object), meaning that it can only operate with children of the object and its own functions.
The solution, then, is to make the top level widget a child of the "controller".
This cannot be directly done with setParent() in your case, though, since the QWidget override of setParent() expects a QWidget as argument, and your MyWin class is a simple QObject instead.
While the theoretical solution could be to call QObject.setParent(self.ui, self) to circumvent the override, this won't work and will sooner or later crash. As explained in this related post, the only available solution is to make the "controller" a QWidget subclass, even if you're not showing it.
Note that there is an alternative solution that should provide the same results as uic.loadUi using a QUiLoader subclass, as explained in this answer. It obviously misses the other PyQt parameters, but for general usage it shouldn't be a problem.
Finally, remember that you could always use the loadUiType function that works exactly like the PyQt one (again, without extra parameters); it's a slightly different pattern, since it generates the class names dynamically, but has the benefit that it parses the ui just once, instead of doing it every time a new instance is created.
With a custom function you can even create a class constructor with the path and return a type that also calls setupUi() on its own:
def UiClass(path):
formClass, widgetClass = loadUiType(path)
name = os.path.basename(path).replace('.', '_')
def __init__(self, parent=None):
widgetClass.__init__(self, parent)
formClass.__init__(self)
self.setupUi(self)
return type(name, (widgetClass, formClass), {'__init__': __init__})
class Win(UiClass('mainWindow.ui')):
def __init__(self):
super().__init__()
# no need to call setupUi()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
test = Win()
test.show()
sys.exit(app.exec())
With the above, print(Win.__mro__) will show the following (this is from PyQt, PySide will obviously have the appropriate module name):
(<class '__main__.Win'>, <class '__main__.mainWindow_ui'>,
<class 'PyQt5.QtWidgets.QMainWindow'>, <class 'PyQt5.QtWidgets.QWidget'>,
<class 'PyQt5.QtCore.QObject'>, <class 'sip.wrapper'>,
<class 'PyQt5.QtGui.QPaintDevice'>, <class 'sip.simplewrapper'>,
<class 'Ui_MainWindow'>, <class 'object'>)
As noted in the comment by #ekhumoro, since PySide6 the behavior has changed, since uic is now able to directly output python code, so you need to ensure that the uic command of Qt is in the user PATH.
PS: note that, with PyQt, using connectSlotsByName() always calls the target function as many overrides as the signal has, which is the case of clicked signal of buttons; this is one of the few cases for which the #pyqtSlot decorator is required, so in your case you should decorate the function with #pyqtSlot(), since you are not interested in the checked argument. For PySide, instead, the #Slot is mandatory in order to make connectSlotsByName() work.
See this related answer.
I'm trying to add a readout of the cursor position in a pqytplot plotwidget in PyQt5. I found this code which does what I want, but in a stand-alone window all within one program file:
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
#generate layout
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
label = pg.LabelItem(justify='right')
win.addItem(label)
p1 = win.addPlot(row=1, col=0)
data1 = [n**2 for n in range(100)]
p1.plot(data1, pen="r")
#cross hair
vLine = pg.InfiniteLine(angle=90, movable=False)
hLine = pg.InfiniteLine(angle=0, movable=False)
p1.addItem(vLine, ignoreBounds=True)
p1.addItem(hLine, ignoreBounds=True)
def mouseMoved(evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
if p1.sceneBoundingRect().contains(pos):
mousePoint = p1.vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index > 0 and index < len(data1):
label.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>" % (mousePoint.x(), data1[index]))
vLine.setPos(mousePoint.x())
hLine.setPos(mousePoint.y())
proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
The problem I'm running in to is figuring out how to implement something like this with my GUI - where I will have to pass reference to the plotwidget to the mouseMoved function. In the example above, the mousemoved function has access to hline, vline and p1, but in my code it won't - I need to be able to pass those through. But I have no idea how to do that.
I've tried to replicate this issue with the smallest amount of code possible. First here's a simple UI file for the GUI, called CursorLayout.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1167</width>
<height>443</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="startbutton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Plot</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="PlotWidget" name="plotWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="exitbutton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Exit</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QWidget</extends>
<header location="global">pyqtgraph</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
The main program is this:
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from initGUI import connecttolayout, setinitialview
class UI(QMainWindow):
def __init__(self):
super(UI, self).__init__()
uic.loadUi("CursorLayout.ui", self) #load GUI layout file created with QtDesigner
connecttolayout(self) # connect code to elements in UI file
setinitialview(self) # set initial view (button/label visibility, default values, etc)
self.show()
def clickedstartButton(self): #action if start button clicked
self.plotWidget.clear()
plotx = range(100)
ploty = [number**2 for number in plotx]
thisline = self.plotWidget.plot(plotx, ploty, pen='r')
QApplication.processEvents()
def clickedexitButton(self):
self.close()
app=QApplication([])
UIWindow=UI()
app.exec()
with file containing code to set up the gui, initGUI.py (not necessarily how you would do this, but this is to mimic the file structure of my larger program):
from PyQt5.QtWidgets import QPushButton
import pyqtgraph as pg
def connecttolayout(self): #connect GUI elements to elements in UI file
self.startButton = self.findChild(QPushButton, "startbutton")
self.exitButton = self.findChild(QPushButton, "exitbutton")
self.startButton.clicked.connect(self.clickedstartButton)
self.exitButton.clicked.connect(self.clickedexitButton)
def mouseMoved(evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
if self.plotWidget.sceneBoundingRect().contains(pos):
mousePoint = self.plotWidget.vb.mapSceneToView(pos)
index = int(mousePoint.x())
#if index > 0 and index < len(data1):
if index > 0 and index < self.MFmax:
self.cursorlabel.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y=%0.1f</span>" % (
mousePoint.x(), mousePoint.y()))
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
def setinitialview(self): #set initial view to pvst view and clear plot window
#set plot initial configuration
self.plotWidget.setBackground('w')
self.plotWidget.setLabels(left=('Pressure', 'Torr'))
self.plotWidget.setLabel('left',color='black',size=30)
self.plotWidget.setLabels(bottom=('Time', 's'))
self.plotWidget.setLabel('bottom',color='black',size=30)
self.plotWidget.clear()
# cross hair
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.plotWidget.addItem(self.vLine, ignoreBounds=True)
self.plotWidget.addItem(self.hLine, ignoreBounds=True)
self.cursorlabel = pg.LabelItem(justify='right')
proxy = pg.SignalProxy(self.plotWidget.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
I'm actually surprised my attempt doesn't cause an error - pressing the plot button does create a plot, but it definitely doesn't create the cursor in the graph in the GUI.
How do I get the necessary info passed to the mouseMoved function?
There are a few little errors that will make your program fail:
The mouseMoved() function has to be inside your widget class because it needs the evt argument, which is generated in the widget.
The self.MFmax variable/constant is not created anywhere
In this line:
mousePoint = self.plotWidget.vb.mapSceneToView(pos)
The PlotWidget object doesn't have the vb attribute. It is a PlotItem's attribute, then you should change that line to this:
mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
Pyqtgraph recommends here to use TextItem instead of LabelItem, to display text inside a scaled view, because of its scaling size.
Now, with that said and reorganizing your code to be more legible, here is my solution to your code (you only need the UI file and this script):
import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, uic
ui_file = uic.loadUiType("CursorLayout.ui")[0]
class UI(QtGui.QMainWindow, ui_file):
def __init__(self):
## Inherit the QMainWindow and ui_file classes
QtGui.QMainWindow.__init__(self)
ui_file.__init__(self)
self.setupUi(self)
## Create aditional widgets
self.plot_item = self.plotWidget.plot()
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.cursorlabel = pg.TextItem(anchor=(-1,10))
## Build the rest of the GUI
self.format_plot()
## data
self.plotx = range(100)
self.ploty = [number**2 for number in self.plotx]
## Connect signals to actions
self.startbutton.clicked.connect(self.clickedstartButton)
self.exitbutton.clicked.connect(self.clickedexitButton)
self.plotWidget.scene().sigMouseMoved.connect(self.mouseMoved)
## OVERWRITE the mouseMoved action:
def mouseMoved(self, evt):
pos = evt
if self.plotWidget.sceneBoundingRect().contains(pos):
mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index > 0 and index < len(self.plotx):
# if index > 0 and index < self.MFmax:
self.cursorlabel.setHtml(
"<span style='font-size: 12pt'>x={:0.1f}, \
<span style='color: red'>y={:0.1f}</span>".format(
mousePoint.x(), mousePoint.y()))
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
def clickedstartButton(self): #action if start button clicked
self.plot_item.setData(self.plotx, self.ploty, pen='r')
self.plotWidget.addItem(self.cursorlabel)
def clickedexitButton(self):
self.close()
def format_plot(self):
self.plotWidget.setBackground('w')
self.plotWidget.setLabels(left=('Pressure', 'Torr'))
self.plotWidget.setLabel('left',color='black',size=30)
self.plotWidget.setLabels(bottom=('Time', 's'))
self.plotWidget.setLabel('bottom',color='black',size=30)
self.plotWidget.addItem(self.vLine, ignoreBounds=True)
self.plotWidget.addItem(self.hLine, ignoreBounds=True)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = UI()
window.show()
sys.exit(app.exec_())
The code above will make the "crosshair" (the hline and vline) to follow your mouse and displaying the coordinates of that position, like this:
If you want the "crosshair" to track the points in the curve based on the x-axis position of your cursor, you can change the mouseMoved() function to this:
def mouseMoved(self, evt):
pos = evt
if self.plotWidget.sceneBoundingRect().contains(pos):
mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
mx = np.array([abs(i-mousePoint.x()) for i in self.plotx])
index = mx.argmin()
if index >= 0 and index < len(self.plotx):
self.cursorlabel.setHtml(
"<span style='font-size: 12pt'>x={:0.1f}, \
<span style='color: red'>y={:0.1f}</span>".format(
self.plotx[index], self.ploty[index])
)
self.vLine.setPos(self.plotx[index])
self.hLine.setPos(self.ploty[index])
And this will be the result:
Performance of mx calculation in mouseMoved can be largly improved in order to get a faster response of the cursor:
def mouseMoved(self, evt):
pos = evt
if self.plotWidget.sceneBoundingRect().contains(pos):
mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
mx = abs(np.ones(len(self.plotx))*mousePoint.x() - self.plotx)
index = mx.argmin()
if index >= 0 and index < len(self.plotx):
self.cursorlabel.setHtml(
"<span style='font-size: 12pt'>x={:0.1f}, \
<span style='color: red'>y={:0.1f}</span>".format(
self.plotx[index], self.ploty[index])
)
self.vLine.setPos(self.plotx[index])
self.hLine.setPos(self.ploty[index])
I am developing a UI for a desktop app. I've seen that "Menus" are deprecated and hence decided to use the Gtk.Application class for the first time ever.
In the following link https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html#example
they provide the following code:
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gio, Gtk
# This would typically be its own file
MENU_XML="""
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="app-menu">
<section>
<attribute name="label" translatable="yes">Change label</attribute>
<item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 1</attribute>
<attribute name="label" translatable="yes">String 1</attribute>
</item>
<item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 2</attribute>
<attribute name="label" translatable="yes">String 2</attribute>
</item>
<item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 3</attribute>
<attribute name="label" translatable="yes">String 3</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">win.maximize</attribute>
<attribute name="label" translatable="yes">Maximize</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">app.about</attribute>
<attribute name="label" translatable="yes">_About</attribute>
</item>
<item>
<attribute name="action">app.quit</attribute>
<attribute name="label" translatable="yes">_Quit</attribute>
<attribute name="accel"><Primary>q</attribute>
</item>
</section>
</menu>
</interface>
"""
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# This will be in the windows group and have the "win" prefix
max_action = Gio.SimpleAction.new_stateful("maximize", None,
GLib.Variant.new_boolean(False))
max_action.connect("change-state", self.on_maximize_toggle)
self.add_action(max_action)
# Keep it in sync with the actual state
self.connect("notify::is-maximized",
lambda obj, pspec: max_action.set_state(
GLib.Variant.new_boolean(obj.props.is_maximized)))
lbl_variant = GLib.Variant.new_string("String 1")
lbl_action = Gio.SimpleAction.new_stateful("change_label", lbl_variant.get_type(),
lbl_variant)
lbl_action.connect("change-state", self.on_change_label_state)
self.add_action(lbl_action)
self.label = Gtk.Label(label=lbl_variant.get_string(),
margin=30)
self.add(self.label)
self.label.show()
def on_change_label_state(self, action, value):
action.set_state(value)
self.label.set_text(value.get_string())
def on_maximize_toggle(self, action, value):
action.set_state(value)
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
**kwargs)
self.window = None
self.add_main_option("test", ord("t"), GLib.OptionFlags.NONE,
GLib.OptionArg.NONE, "Command line test", None)
def do_startup(self):
Gtk.Application.do_startup(self)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
builder = Gtk.Builder.new_from_string(MENU_XML, -1)
self.set_app_menu(builder.get_object("app-menu"))
def do_activate(self):
# We only allow a single window and raise any existing ones
if not self.window:
# Windows are associated with the application
# when the last one is closed the application shuts down
self.window = AppWindow(application=self, title="Main Window")
self.window.present()
def do_command_line(self, command_line):
options = command_line.get_options_dict()
# convert GVariantDict -> GVariant -> dict
options = options.end().unpack()
if "test" in options:
# This is printed on the main instance
print("Test argument recieved: %s" % options["test"])
self.activate()
return 0
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog(transient_for=self.window, modal=True)
about_dialog.present()
def on_quit(self, action, param):
self.quit()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Where they show the following image as supossed output:
Yet I'm running it and I'm getting this window displayed:
I'm just bashing "python3 application.py"
I guess I'd like to know if either I'm dumb or the code isn't what should be anymore.
It must be a problem with your distro and/or window manager, because in Linux Mint 19 I get this window:
How can I change an image at runtime with Glade?
I'm trying to prompt a "refresh" of an image in my GUI when a button is pressed. The idea is that my handler will do some work on the image, and once it is complete, the image will load in the new image data from file.
I've been working from this post to try and accomplish this, but with no success.
Below is a stripped-down version of what I am working with. I am simply trying to load a second image whenever the button is pressed. I have also tried creating a new PixBuf with the image filepath, assigning it to the image widget, and adding that back to the parent.
I have verified that the builder is retrieving the image correctly.
import gi
gi.require_version('Gtk','3.0')
from gi.repository import Gtk, GdkPixbuf
STREETMAP = "/home/testrun/testing/output.png"
RADARMAP = "/home/testrun/testing/radar.png"
# The glade file to load widgets from
GLADEFILE = "slideshow.glade"
def get_builder(filename):
builder = Gtk.Builder()
builder.add_from_file(filename)
return builder
class mapWindow:
def __init__(self):
self.builder = get_builder(GLADEFILE)
self.toggle = 1
# Define signal mappings for builder
self.handlers = {
"quit" :self.quit,
"change" :self.change
}
self.window = self.builder.get_object("window1")
self.builder.connect_signals(self.handlers)
def quit(self, *args):
Gtk.main_quit(*args)
def change(self, button):
image_file = ""
if(1 == self.toggle):
self.toggle = 0
image_file = RADARMAP
else:
self.toggle = 1
image_file = STREETMAP
print("Changing image to %s" % image_file)
builder = get_builder(GLADEFILE)
mapImg = builder.get_object("image1")
parent = builder.get_object("box1")
parent.remove(mapImg)
mapImg.clear()
mapImg.set_from_file(image_file)
parent.add(mapImg)
mapImg.show()
self.window.show_all()
print("Image rendered")
myWindow = mapWindow()
myWindow.window.show_all()
Gtk.main()
And here is my .glade file:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<signal name="delete-event" handler="quit" swapped="no"/>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixbuf">radar.png</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button1">
<property name="label" translatable="yes">Change</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="pressed" handler="change" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
Move the glade widgets from the method change() to the __init__() method and let them be object properties, no need to always retrieve the same widget.
Once you have it as a property, then on the change callback method just change the image. Notice that if the image sizes differ, the window will readjust it's size if the second image is bigger but won't shrink if smaller.
Check the code between the hash lines:
def __init__(self):
self.builder = get_builder(GLADEFILE)
...
#############################################################
self.window = self.builder.get_object("window1")
self.mapImg = self.builder.get_object("image1")
self.parent = self.builder.get_object("box1")
self.builder.connect_signals(self.handlers)
self.window.show_all()
#############################################################
...
...
...
def change(self, button):
image_file = ""
if(1 == self.toggle):
self.toggle = 0
image_file = RADARMAP
else:
self.toggle = 1
image_file = STREETMAP
print("Changing image to %s" % image_file)
#############################################################
self.mapImg.set_from_file(image_file)
#############################################################
print("Image rendered")
Given the following XML how can I obtain the value of status with Groovy's XmlSlurper:
<response status="success">
<child>
<age>21</age>
<name>Jane McCoy</name>
</child>
</response>
def xmlStr = """
<response status="success">
<child>
<age>21</age>
<name>Jane McCoy</name>
</child>
</response>
"""
def parsed = new XmlSlurper().parseText(xmlStr)
assert parsed.'#status' == 'success'