How to place wxChoice in a wxGrid header? - header

I'd like to creat a grid, in which user can change column name by wxchoice (or wxcombo) control.
I imagine it like this:
for example let user has tree columns of data
John,Smith,190
Maria,Kowalsky,180
I'd like to let user match each column to one of three options (firstname, lastname, height)
I'm at very beginning:
#!/usr/bin/python
# coding: utf-8
import wx
from wx.grid import Grid
class DocsVarValGrid(Grid):
"""
"""
def __init__(self, parent, init_data=None, *args, **kwargs):
super(DocsVarValGrid, self).__init__(parent, *args, **kwargs)
self.CreateGrid(1, 1)
self.cols_names = init_data
class MyFrame(wx.Frame):
""""""
def __init__(self, *args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
#self.panel = PickAFile(parent=self)
self.grid = DocsVarValGrid(self, init_data=['a', 'b', 'c'])
self.Layout()
def main():
app = wx.App() # creation of the wx.App object (initialisation of the wxpython toolkit)
frame = MyFrame(None, title="Hello World") # creation of a Frame with "Hello World" as title
frame.Show() # frames are invisible by default so we use Show() to make them visible
app.MainLoop() # here the app enters a loop waiting for user input
if __name__ == '__main__':
main()

You can't actually do that. Instead you would have to draw a combobox yourself. This is allowed using the GridLabelRenderer mixin. At least, that's what is implied in the wxPython demo. If you don't already have that, I recommend downloading it and checking out the examples there. It doesn't actually have a combobox in that example, but I think it will get you going.

The column header window can be fetched from the grid using grid.GetGridColLabelWindow() and you can then do whatever you want with that window, such as override its paint event or placing widgets on it. Take a look at the wx.lib.mixins.gridlabelrenderer module for some helper classes that would help you with overriding the drawing of the labels.
So while drawing a combobox-like thing yourself would be doable, in your case it would probably make most sense to just put a real widget there. You will have to manage its size and position yourself, and adjust it every time the grid and/or columns are resized, but it should not be too overly difficult. If you look in the gridlabelrenderer you will be able to see some code that calculates the rectangle that represents each label, and you can use something like that to resize and reposition your widget.

Related

How to separate between python generated ui and our apps logic in pyqt5? [duplicate]

I'm using Qt Designer for design GUI to use in python, after designing my desired UI in Qt Designer, convert it to python code and then I changed generated code to do some action in my python code, but if I changed the UI with Qt Designer and convert it to python code again, I lost my previous changes on my code.
how can I solve the problem?
can we Spreading a Class Over Multiple Files in python to write code in other files?
To avoid having these problems it is advisable not to modify this file but to create a new file where we implement a class that uses that design.
For example, suppose you have used the MainWindow template in the design.ui file, then convert it to Ui_Design.py like to the following structure:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
[...]
def retranslateUi(self, MainWindow):
[...]
Then we will create a new file that we will call logic.py where we will create the file that handles the logic and that uses the previous design:
class Logic(QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
QMainWindow.__init__(self, *args, **kwargs)
self.setupUi(self)
So even if you modify the design and generate the file again .py you will not have to modify the file of the logic.
To generalize the idea we must have the following rules but for this the logic class must have the following structure:
class Logic(PyQtClass, DesignClass):
def __init__(self, *args, **kwargs):
PyQtClass.__init__(self, *args, **kwargs)
self.setupUi(self)
PyQtClass: This class depends on the design chosen.
Template PyQtClass
─────────────────────────────────────────────
Main Window QMainWindow
Widget QWidget
Dialog with Buttons Bottom QDialog
Dialog with Buttons Right QDialog
Dialog with Without Buttons QDialog
DesignClass: The name of the class that appears in your design.
The advantage of this implementation is that you can implement all the logic since it is a widget, for example we will implement the solution closing pyqt messageBox with closeevent of the parent window :
class Logic(QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
QMainWindow.__init__(self, *args, **kwargs)
self.setupUi(self)
def closeEvent(self, event):
answer = QtWidgets.QMessageBox.question(
self,
'Are you sure you want to quit ?',
'Task is in progress !',
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if answer == QtWidgets.QMessageBox.Yes:
event.accept()
else:
event.ignore()
The easiest way is to use the *.ui file directly in the python code, you don't need convert to *.py file every time you change the ui.
you can use this pseudo code in your project.
# imports
from PyQt5 import uic
# load ui file
baseUIClass, baseUIWidget = uic.loadUiType("MainGui.ui")
# use loaded ui file in the logic class
class Logic(baseUIWidget, baseUIClass):
def __init__(self, parent=None):
super(Logic, self).__init__(parent)
self.setupUi(self)
.
.
.
.
def main():
app = QtWidgets.QApplication(sys.argv)
ui = Logic(None)
ui.showMaximized()
sys.exit(app.exec_())

pyqt5 QLabel Image setScaledContents(True) don't allow Qpainter updates

I want to display an image and put a marker at the current mouse position for every left mouse click.
Below code does the job however, it works only if ("self.imglabel.setScaledContents(True)") is commented. Any reason?
I have to do this job on various images of different resolutions, I read to maintain the proper aspect ratio and display the image appropriately we need to use setScaledContents(True). But why enabling this is not allowing update() (PaintEvent)??
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QSizePolicy, QMessageBox
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QImage, QPalette
class Menu(QMainWindow):
def __init__(self):
super().__init__()
self.central_widget = QWidget() # define central widget
self.setCentralWidget(self.central_widget)
self.vbox = QVBoxLayout(self.central_widget)
self.vbox.addWidget(self.imgWidget())
self.vbox.addWidget(QPushButton("test"))
def imgWidget(self):
self.imglabel = QLabel()
self.imglabel.setScaledContents(True)
self.image = QImage("calib.jpeg")
self.imagepix = QPixmap.fromImage(self.image)
self.imglabel.setPixmap(self.imagepix)
self.imglabel.mousePressEvent = self.imgMousePress
return self.imglabel
def imgMousePress(self, e):
painter = QPainter(self.imglabel.pixmap())
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
painter.drawPoint(e.x(), e.y())
painter.end()
self.imglabel.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainMenu = Menu()
mainMenu.show()
sys.exit(app.exec_())
To avoid unnecessary computation for each paintEvent of the QLabel, whenever the scaledContents property is True the scaled image is cached, and all the painting is automatically discarded.
To avoid that, you should create a new instance of QPixmap using the existing one, and then set the new painted pixmap again. Note that if the image is scaled, the widget coordinates won't reflect the actual position on the pixmap, so you need to use a transformation to get the actual point to paint at.
def imgMousePress(self, e):
pm = QPixmap(self.imglabel.pixmap())
painter = QPainter(pm)
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
transform = QTransform().scale(
pm.width() / self.imglabel.width(),
pm.height() / self.imglabel.height())
painter.drawPoint(transform.map(e.pos()))
painter.end()
self.imglabel.setPixmap(pm)
Consider that all the "points" will become stretched rectangles if the width/height ratio is not the same of the source image, but this is only a problem of appearance: if you save the pixmap later, they will be squares again, since saving is based on the source pixmap.
If you want to keep their squared shape while displaying instead, you'll need to keep track of the points and overwrite paintEvent to paint them manually on the label.

matplotlib animation embedded in tkinter : update function never called

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method
As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

Embedding transparent matplotlib figure canvas in wx

I have a GUI written in wxPython with a matplotlib figure embedded. I want the background color of the figure to be the same as the rest of the (quite large) GUI. Unfortunately the exact color of the GUI is OS-dependent, so it is not enough to set a fixed background color since it will change with the OS. Therefore I tried to use facecolor='none' when creating the matplotlib-figure. However that gave some unexpected problems (see image below): every time you redraw the canvas the label text and tick marks is getting thicker as if the weight of the font is changing. I found this three years old question, which seems to deal with a very similar problem, but it does not have any solution nor comments of what to do. Is this an intended feature of matplotlib/wxpython or just a bug which as not yet been fixed?
Example code to show the problem. I create two FigureCanvases, where the first one has a facecolor='b background, and with it the text is not getting thicker when redrawing the canvas. The second canvas is using facecolor='none', and gives thicker and thicker text for each canvas redraw.
import wx
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
class MyCanvas(wx.Panel):
def __init__(self, parent, col):
wx.Panel.__init__(self, parent, id=-1)
self.fig = Figure(figsize=(1, 1), edgecolor='k', facecolor=col)
self.ax = self.fig.add_subplot(1, 1, 1)
self.ax.set_ylabel('Label')
self.fig.subplots_adjust(left=0.5)
self.canvas = FigureCanvasWxAgg(self, -1, self.fig)
szr_ctr = wx.BoxSizer(wx.VERTICAL)
szr_ctr.Add(self.canvas, 1, wx.ALL | wx.GROW)
self.SetSizerAndFit(szr_ctr)
wx.CallAfter(self.canvas.draw)
class wind(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent)
self.i = 0
figsizer = wx.BoxSizer(wx.HORIZONTAL)
self.canvas1 = MyCanvas(self, col='b')
figsizer.Add(self.canvas1, 1, wx.GROW | wx.ALL)
self.canvas2 = MyCanvas(self, col='none')
figsizer.Add(self.canvas2, 1, wx.GROW | wx.ALL)
button = wx.Button(self, wx.ID_CLOSE, "Press me")
button.Bind(wx.EVT_BUTTON, self.on_button)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(figsizer, 1, wx.ALL | wx.GROW, 10)
sizer.Add(button, 0, wx.ALL, 10)
self.SetSizer(sizer)
self.Layout()
self.Show()
def on_button(self, event):
wx.CallAfter(self.canvas1.canvas.draw)
wx.CallAfter(self.canvas2.canvas.draw)
if __name__ == '__main__':
wxapp = wx.App(redirect=False)
v = wind(None, "Fig")
wxapp.MainLoop()
Figure with blue background works as expected. Figure with none background gets thicker and thicker texts and axes-lines after a few canvas redraws.
Edit
Changing the redraw-function to (below) solves the problem with the canvas not being properly redrawn.
def on_button(self, event):
wx.CallAfter(self.canvas1.canvas.draw)
wx.CallAfter(self.canvas2.canvas.draw)
wx.CallAfter(self.canvas1.Refresh) # <-----------
wx.CallAfter(self.canvas2.Refresh) # <-----------
After fiddling around a bit more, I realised the problem can be solved by using self.canvas_i.Refresh() after self.canvas_i.canvas.draw(). As far as I understand Refresh will mark the canvas as "in need of redrawing", forcing it to be repainted completely. This overpaints any old content and makes it just one iteration old (every new canvas.draw just draws the new thing ontop of the old, slightly placed to the side giving "thicker text").

Matplot lib does not plot with PyQt

I'm having a problem with PyQt and Mathplotlib.
Here you can find a pseudocode of what I am doing: I have a class "MainWindow" that creates a main window with a menu and an empty mathplotlib graph. When I click on the menu Item, the method "Select" is executed that opens a new Dialog. There is also a method that plots on the grah the content of the global variable Data.
import TeraGui
Data = []
class MainWindow(QMainWindow, TeraGui.Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.actionSelect.triggered.connect(self.Select)
# Create the frame with the graphs
self.create_main_frame()
#Plot empty graphs
self.axes.clear()
self.canvas.draw()
def create_main_frame(self):
self.main_frame = QWidget()
# Create the mpl Figure and FigCanvas objects.
# 5x4 inches, 100 dots-per-inch
#
self.dpi = 100
self.fig = Figure((5.0, 4.0), dpi=self.dpi)
self.canvas = FigureCanvas(self.fig)
self.canvas.setParent(self.main_frame)
#
self.axes = self.fig.add_subplot(111)
# Create the navigation toolbar, tied to the canvas
#
self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame)
#
# Layout with box sizers
#
vbox = QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.mpl_toolbar)
self.main_frame.setLayout(vbox)
self.setCentralWidget(self.main_frame)
def Plotting(self):
""" Redraws the figure
"""
print "I am here"
time = Data[0]
sig = Data[]
plot(time, sig)
# clear the axes and redraw the plot anew
#
self.axes.clear()
self.axes.plot(time, sig)
self.canvas.draw()
def Select(self):
dialog = Dialog(self)
dialog.exec_()
Now, if I add in the init method of the MainWindow class these lines:
Global Data
Data = [[1,2,3],[4,5,6]]
self.Plotting()
"I am here" is printed and the plot is correctly displayed into the graph, BUT if I don't add these lines and i try to call Plotting from the Dialog class it doesn't work. "I am here" is plotted but the plot stays empty. In the Dialog class, method "accept" is caled when the "ok" button of a button box is pressed:
class Dialog(QDialog, TeraGui.Ui_SelectFiles):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setupUi(self)
def accept(self):
global Data
Data = [[1,2,3],[4,5,6]]
MainWindow().Plotting()
The Plotting method draws also a separate plot by means of the command "plot(time,sig)". This plot is always showed correctly regardless the way used to call Plotting.
These are my fist tries with PyQt and matplotlib and I am not able to identify the mistake.
The problem is with the line
MainWindow().Plotting()
When you write MainWindow() you are actually creating a new instance of the MainWindow class and calling its Plotting() function, not the one of your existing MainWindow instance. This window is never shown, and since you don't save a reference to it, is subsequently deleted when accept() returns. The only evidence of its existence is the 'i am here' message it writes to the console. This is why you don't see the plot.
In this case, you are setting your MainWindow instance as the parent of dialog through dialog = Dialog(self), so you could access it though a call to parent().
self.parent().Plotting()
You should also consider adding another parameter to the Plotting() function so you can pass your data directly to it instead of having to declare globals everywhere.
def Plotting(self, Data):
...