PyQt5: How to Get Dimensions of Displayed Widgets - pyqt5

I have PyQt5 QLabels that expand/contract as the QMainWindow size changes. I want to get the dimensions of the QLabels when the QMainWindow is sized to anything other that its initial dimensions.
The script below creates two QLabels in a QMainWindow. The QLabels expand but the top QLabel has a fixed height. The QMainWindow is created with dimensions 400x300 and and displays as screen maximized using showMaximized() (in my case, 1920 x 1080). The script prints the dimensions of the QLabels before and after the QMainWindow displays. Prior to display, width() and height() return default QLabel values and after display (screen maximized) width() and height() return values as if the QMainWindow has physical dimensions of 400x300. Here is wat is printed:
label_1 Size Before Expanding: 100 100
label_2 Size Before Expanding: 100 30
label_1 Size After Expanding: 378 100
label_2 Size After Expanding: 378 171
How can I get the true dimensions for the QLabels when the QMainWindow is maximized? I'm running in a Windows environment.
from PyQt5.QtWidgets import QApplication, QMainWindow, QSizePolicy, QLabel, QVBoxLayout, QWidget
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.setGeometry(100, 100, 400, 300)
self.layout = QVBoxLayout(central_widget)
self.label_1 = QLabel(self)
self.label_1.setStyleSheet('background-color: green')
self.label_1.setFixedHeight(100)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.label_1.setSizePolicy(sizePolicy)
self.layout.addWidget(self.label_1)
self.label_2 = QLabel(self)
self.label_2.setStyleSheet('background-color: red')
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.label_2.setSizePolicy(sizePolicy)
self.layout.addWidget(self.label_2)
print('label_1 Size Before Expanding: ', self.label_1.width(), self.label_1.height())
print('label_2 Size Before Expanding: ', self.label_2.width(), self.label_2.height())
self.showMaximized()
print('label_1 Size After Expanding: ', self.label_1.width(), self.label_1.height())
print('label_2 Size After Expanding: ', self.label_2.width(), self.label_2.height())
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
app.exec()

When widgets are created but not yet mapped ("shown") on the screen, they always have a default size:
100x30 for all widgets that have a parent explicitly set (either by adding the parent argument in the constructor, or by calling setParent());
640x480 for all top level widgets (widgets that have no parent explicitly set);
The only exception is when size constraints exist, like in your case: the first label has a fixed height, and that's what is shown in the output (remember that "fixed size" means that both minimum and maximum sizes are the same).
Calling show() or setVisible(True) the first time, automatically creates (amongst others) a Resize event on the top level or parent widget, which automatically activates the layout for that widget and eventually recursively creates Resize events for all widgets managed by its layout, based on the layout computations. This does not happen before showing the widgets the first time.
This is done for optimization reasons: updating a layout can be very demanding, especially for complex UIs with multiple nested layouts that contain widgets that have different size hints, policies, stretches, etc.
Since geometries will be updated anyway as soon as the window will be shown, there's no point in setting a size until the full layout has been completed. Also note that using setGeometry() or resize() prior showing the widget the first time will not activate the layout, as explained above.
That said, it is possible to update sizes based on the current layout even if widgets have not been shown yet: you have to explicitly activate() the layout manager.
But, be aware: in order to get the correct sizes based on the layout, you need to activate all layouts, up to the top level widget. QMainWindow has its own private layout, so you need to activate that too.
Since you've overwritten the default layout() function with self.layout, the only way to access it is through the super() call.
Then, there's another problem: functions that change the window state (maximized, minimized, full screen and normal) do not directly resize the window. Those functions (including setWindowState()) actually "ask" the OS to change the window state, then the OS will decide on its own if the request is acceptable and eventually resize the window according to its behavior based on the requested state.
That resizing will happen at an undefined point after that call, and there's no direct way to know when: the OS might have some fancy animation to show the state change, and that might cause continuous changes in the size or even an abrupt change to the new size after that "process" has finished. Even using processEvents() won't be enough, since that function only processes events directly handled by Qt, and Qt cannot know anything about external OS events.
The only way to know for sure the size of widgets after any resizing, is by overriding the resizeEvent().
class MainWindow(QMainWindow):
def __init__(self, parent=None):
# ...
super().layout().activate()
self.layout.activate()
print('label_1 Size Before Showing: ', self.label_1.size())
print('label_2 Size Before Showing: ', self.label_2.size())
self.showMaximized()
def resizeEvent(self, event):
super().resizeEvent(event)
print('label_1 Size After Showing/Resizing: ', self.label_1.size())
print('label_2 Size After Showing/Resizing: ', self.label_2.size())
This will correctly print, before showMaximized():
label_1 Size Before Expanding: PyQt5.QtCore.QSize(388, 100)
label_2 Size Before Expanding: PyQt5.QtCore.QSize(388, 182)
label_1 Size After Resizing: PyQt5.QtCore.QSize(388, 100)
label_2 Size After Resizing: PyQt5.QtCore.QSize(388, 182)
label_1 Size After Resizing: PyQt5.QtCore.QSize(1428, 100)
label_2 Size After Resizing: PyQt5.QtCore.QSize(1428, 757)
Note that the resizeEvent is called twice: the first one is right after any show*() call, the second is when the window has been actually maximized. If you remove the activate calls above, the first output will be the same as the default values explained at the beginning.

To get the true size, I think you need to trigger off of resizeEvent. I don't know of a way to force the event loop to finish maximizing before you query the size. Even app.processEvents() seems to have no effect on this when you run it in __init__:
from PyQt5.QtWidgets import QApplication, QMainWindow, QSizePolicy, QLabel, QVBoxLayout, QWidget
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.setGeometry(100, 100, 400, 300)
self.layout = QVBoxLayout(central_widget)
self.label_1 = QLabel(self)
self.label_1.setStyleSheet('background-color: green')
self.label_1.setFixedHeight(100)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.label_1.setSizePolicy(sizePolicy)
self.layout.addWidget(self.label_1)
self.label_2 = QLabel(self)
self.label_2.setStyleSheet('background-color: red')
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.label_2.setSizePolicy(sizePolicy)
self.layout.addWidget(self.label_2)
self.show()
# ~ print('label_1 Size Before Expanding: ', self.label_1.width(), self.label_1.height())
# ~ print('label_2 Size Before Expanding: ', self.label_2.width(), self.label_2.height())
self.showMaximized()
# ~ print('label_1 Size After Expanding: ', self.label_1.width(), self.label_1.height())
# ~ print('label_2 Size After Expanding: ', self.label_2.width(), self.label_2.height())
def resizeEvent(self, event):
print('label_1 Size : ', self.label_1.width(), self.label_1.height())
print('label_2 Size : ', self.label_2.width(), self.label_2.height())
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
which gives
label_1 Size : 100 100
label_2 Size : 100 30
label_1 Size : 1268 100
label_2 Size : 1268 869

Related

PyQt5 rotation pixmap

In Pyqt5 I want to rotate a pixmap but every time i tried it changes the size.
My code is:
import math
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
from PyQt5.QtCore import QObject, QPointF, Qt, QRectF,QRect
from PyQt5.QtGui import QPixmap, QTransform, QPainter
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__()
self.arch1 = QPixmap("arch1.png")
pm = QPixmap(556,556)
rectF = QRectF(0,0,556,556)
painter = QPainter(pm)
painter.drawPixmap(rectF, self.arch1, rectF)
painter.end()
self.label = QLabel("AAAAAAAAAA")
self.label.setPixmap(pm)
butA = QPushButton("A")
butA.clicked.connect(lambda: self.rotate_item())
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(butA)
self.setLayout(layout)
self.show()
def rotate_item(self):
rectF = QRectF(0,0,556,556)
self.arch1 = self.arch1.transformed(QTransform().rotate(36))
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.drawPixmap(rectF, self.arch1,QRectF(self.arch1.rect()))
painter.end()
self.label.setPixmap(pix)
def main():
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
if __name__=="__main__":
main()
I want only rotate not resize. What do you suggest me to do?
I have four other files and i want to rotate differently. i post some photos to understand what i want to do.
any other way to do this?
Circle one
Circle two
Complete circle
The problem is that the rotated pixmap has a bigger bounding rectangle.
Consider the following example:
The light blue square shows the actual bounding rectangle of the rotated image, which is bigger.
Using drawPixmap(rectF, self.arch1, QRectF(self.arch1.rect())) will cause to painter to draw the pixmap in the rectangle rectF, using the new bounding rectangle as source, so it becomes "smaller".
Instead of rotating the image, you should rotate the painter. Since transformations by default use the origin point of the painter (0, 0), we need first to translate to the center of the rectangle, rotate the painter, and then retranslate back to the origin.
Note that in the following example I'm also always drawing starting from the source image, without modifying it: continuously applying a transformation will cause drawing artifacts due to aliasing, and after some rotation the quality would be very compromised.
The rotation variable is to keep track of the current rotation.
class Window(QWidget):
def __init__(self, *args, **kwargs):
# ...
self.rotation = 0
def rotate_item(self):
self.rotation = (self.rotation + 36) % 360
rectF = QRectF(0,0,556,556)
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.translate(rectF.center())
painter.rotate(self.rotation)
painter.translate(-rectF.center())
painter.drawPixmap(0, 0, self.arch1)
painter.end()
self.label.setPixmap(pix)

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.

Can I draw TKinter objects on top of an embedded FigureCanvasTkAgg?

shortly said:
I am creating a quick TKinter API and I firstly generate a tk.Canvas
I am embedding a FigureCanvasTkAgg canvas with master = tk.Canvas above
With this I am able to show an image via Matplotlib
Now I want to draw TKinter objects ON TOP of the FigureCanvasTkAgg canvas (e.g. rectangles or buttons)
Is this possible? Or is there any particular recommendation (i.e. using only one canvas or the other)?
Here some quick code:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
a = self.figure_obj.add_axes([0, 0, 1, 1])
imgplot = a.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# attempt to draw rectangle
self.rectangle = self.canvas.create_rectangle(0, 0, 100, 100, fill='red')
if __name__ == "__main__":
app = MyApp()
app.draw_image()
app.mainloop()
I mean I see that the rectangle is being drawn before the image. Maybe its my lack of understanding on how FigureCanvasTkAgg is attached to tk.canvas
Thank you!
Ok, this is an app that I recently developed where I have matplotlib widgets and mouse events. You can also have tkinter widgets but I didn't find a way to put them on top of the matplolib canvas. Personally I like matplotlib widgets more than tkinter widgets, so I think it is not too bad.
The only pre-step that you have to take is to modify matplotlib source code because you need pass the canvas to the widget class, while by default the widget takes the figure canvas which will not work when embedding in tk (button would be unresponsive). The modification is actually quite simple, but let's go in order.
Open 'widgets.py' in the matplotlib folder (depending on where you installed it, in my case I have it in "C:\Program Files\Python37\Lib\site-packages\matplotlib").
Go to the class AxesWidget(Widget) (around line 90) and modify the __init__ method with the following code:
def __init__(self, ax, canvas=None):
self.ax = ax
if canvas is None:
self.canvas = ax.figure.canvas
else:
self.canvas = canvas
self.cids = []
As you can see compared to the original code I added a keyword argument canvas=None. In this way the original functionality is mantained, but you can now pass the canvas to the widget.
To have a responsive button on the matplolib canvas that is embedded in tk you now create a widget and you pass the matplolib canvas created with FigureCanvasTkAgg. For example for a Buttonyou would write
from matplotlib.widgets import Button
ok_button = Button(ax_ok_button, 'Ok', canvas=canvas) # canvas created with FigureCanvasTkAgg
Ok now we have all the functionalities required to have matplolib widgets on the matplolib canvas embedded in tk, plus you can also have mouse and key events, which I guess covers 95% of what you expect from a GUI. Note that if you don't want to modify the original source code you can, of course, create your own class copying AxesWidget class.
You find all the available matplolib widgets here https://matplotlib.org/3.1.1/api/widgets_api.html
Here is a modified version of your app where we put everything together:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from matplotlib.widgets import Button
import numpy as np
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
self.ax = self.figure_obj.add_subplot()
self.figure_obj.subplots_adjust(bottom=0.25)
some_preloaded_data_array = np.zeros((600,600))
imgplot = self.ax.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib toolbar
toolbar = NavigationToolbar2Tk(self.canvas_agg, self.canvas)
toolbar.update()
self.canvas_agg._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib widgets
self.ax_ok_B = self.figure_obj.add_subplot(position=[0.2, 0.2, 0.1, 0.03]) # axes position doesn't really matter here because we have the resize event that adjusts widget position
self.ok_B = Button(self.ax_ok_B, 'Ok', canvas=self.canvas_agg)
# add tkinter widgets (outside of the matplolib canvas)
button = tk.Button(master=self, text="Quit", command=self._quit)
button.pack(side=tk.BOTTOM)
# Connect to Events
self.ok_B.on_clicked(self.ok)
self.canvas_agg.mpl_connect('button_press_event', self.press)
self.canvas_agg.mpl_connect('button_release_event', self.release)
self.canvas_agg.mpl_connect('resize_event', self.resize)
self.canvas_agg.mpl_connect("key_press_event", self.on_key_press)
self.protocol("WM_DELETE_WINDOW", self.abort_exec)
def abort_exec(self):
print('Closing with \'x\' is disabled. Please use quit button')
def _quit(self):
print('Bye bye')
self.quit()
self.destroy()
def ok(self, event):
print('Bye bye')
self.quit()
self.destroy()
def press(self, event):
button = event.button
print('You pressed button {}'.format(button))
if event.inaxes == self.ax and event.button == 3:
self.xp = int(event.xdata)
self.yp = int(event.ydata)
self.cid = (self.canvas_agg).mpl_connect('motion_notify_event',
self.draw_line)
self.pltLine = Line2D([self.xp, self.xp], [self.yp, self.yp])
def draw_line(self, event):
if event.inaxes == self.ax and event.button == 3:
self.yd = int(event.ydata)
self.xd = int(event.xdata)
self.pltLine.set_visible(False)
self.pltLine = Line2D([self.xp, self.xd], [self.yp, self.yd], color='r')
self.ax.add_line(self.pltLine)
(self.canvas_agg).draw_idle()
def release(self, event):
button = event.button
(self.canvas_agg).mpl_disconnect(self.cid)
print('You released button {}'.format(button))
def on_key_press(self, event):
print("you pressed {}".format(event.key))
# Resize event is needed if you want your widget to move together with the plot when you resize the window
def resize(self, event):
ax_ok_left, ax_ok_bottom, ax_ok_right, ax_ok_top = self.ax.get_position().get_points().flatten()
B_h = 0.08 # button width
B_w = 0.2 # button height
B_sp = 0.08 # space between plot and button
self.ax_ok_B.set_position([ax_ok_right-B_w, ax_ok_bottom-B_h-B_sp, B_w, B_h])
print('Window was resized')
if __name__ == "__main__":
app = MyApp()
app.draw_image_and_button()
app.mainloop()
Ok let's see the functionalities of this app:
Press a key on the keyboard → print the pressed key
Press a mouse button → print the pressed button (1 = left, 2 = wheel, 3 = right)
Release a mouse button → print the released button
Press the right button on any point on the plot and draw a line while keeping the mouse button down
Press ok or quit to close the application
Pressing 'x' to close the window is disabled.
Resize the window → Plot and widgets scales accordingly
I also took the liberty to add the classic matplotlib toolbar for other functionalities like zooming.
Note that the image plot is added with add_suplot() method which adds the resizing functionality. In this way when you resize the window the plot scales accordingly.
Most of the things I implemented you also find them on the official tutorial from matplotlib on how to embed in tk (https://matplotlib.org/3.1.3/gallery/user_interfaces/embedding_in_tk_sgskip.html).
Let me know if this answers your question. I wanted to share it because I actually developed something very similar a few days ago.

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").

How to place wxChoice in a wxGrid 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.