I have a wxPython GUI with a grid in it. I can't predict the exact size of the grid before it is created. My problem is the way the grid scrollbars display when the user clicks on a cell to edit. This is particularly apparent the grid has only one row, which can happen occasionally; if you select a cell to edit, the scrollbars pop up in such a way that you can't see the row at all. I don't want the user to have to scroll within the grid at all (in my GUI, the grid is in a wx.ScrolledWindow).
I need to have some buttons and directions outside of the grid, so having the grid object 'stand alone' is not a good option for me.
Here is the code:
class Spacing(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Fix spacing")
self.panel = wx.ScrolledWindow(self)
self.InitUI()
def InitUI(self):
label = wx.StaticText(self.panel,label="hello")#, size=(500, 50))
grid = self.make_table(1, 5)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(label, flag=wx.ALL, border=10)
vbox.Add(grid, flag=wx.BOTTOM|wx.EXPAND, border=20)
hbox_all= wx.BoxSizer(wx.HORIZONTAL)
hbox_all.AddSpacer(20)
hbox_all.AddSpacer(vbox)
hbox_all.AddSpacer(20)
self.panel.SetSizer(hbox_all)
self.panel.SetScrollbars(20, 20, 50, 50)
hbox_all.Fit(self)
self.Show()
def make_table(self, n_cols, n_rows):
grid = wx.grid.Grid(self.panel, -1)
grid.ClearGrid()
grid.CreateGrid(n_cols, n_rows)
for n in range(n_cols):
for num in range(n_rows):
grid.SetCellValue(n, num, (str(num) + str(n)))
return grid
if __name__ == "__main__":
app = wx.PySimpleApp()
app.frame = Spacing()
app.frame.Show()
app.frame.Center()
app.MainLoop()
I can imagine a few options. One, if I could add some "padding" to the grid window, the user would still be able to see what they were editing no matter what. Two, if I could override the scrolled window nature of the grid, that would work also. I haven't figured
out how to do either of these, and I'm open to other suggestions. I've tried messing with disabling grid.SetAutoLayout and manually sizing the grid, but neither of these has proven effective. I would really appreciate any suggestions!
The (very) hack-y solution I've devised is to make sure to have another widget in the container window that is wider than the grid and also set the grid to wx.EXPAND, which prevents the horizontal scrollbars from ever popping up.
That is, I replace this line of code:
label = wx.StaticText(self.panel,label="hello")
with this:
label = wx.StaticText(self.panel,label="hello", size=(500, 50))
and this line:
vbox.Add(grid, flag=wx.BOTTOM, border=20)
with this:
vbox.Add(grid, flag=wx.BOTTOM|wx.EXPAND, border=20)
This option looks a little weird too, but it doesn't obscure what the user is typing. Obviously this is not a good solution, but I haven't been able to figure out how to actually fix the problem.
Update:
If I create the grid with a set spacing it doesn't re-size:
grid = wx.grid.Grid(self.panel, -1, size = (500, 300))
But if I create the grid and then try to re-size it, it doesn't "stick" and I have the same objectionable behavior as before. In fact, grid.SetSize doesn't seem to work at all visually, even though when I call grid.GetSize() it reports being the size I have assigned.
grid.SetSize(wx.Size(500, 300))
Related
Python 3.8 x64 | Windows 10 x64 | wxPython version 4.1.0
Been searching everywhere for an answer on this to no avail. I created a simple wx.grid.Grid() widget and added a border around the grid. The issue is there's dead space included between the grid widget and it's border towards the bottom of the grid. I am not describing the space around the outside of the border which is added using sizers. The border I chose is wx.BORDER_THEME and I don't want to change this since the rest of my GUI utilizes this same border. I do have the row size set to 20, but the issue persists no matter what the size of the row is changed to.
I've included a minimal, reproducible code example below. It looks like a lot at first glance, but it really isn't and is very basic.
Also attached is a picture illustrating what I attempted to describe above. The bottom line in the attached photo is the border and the upper line is the grid.
Minimal Code Example:
import wx
import wx.grid
class MyGrid(wx.Frame):
def __init__(self):
super().__init__(parent=None, title='Grid With Undesirable Space', size=(650, 300))
self.SetMinSize(self.GetSize()) # set min size of frame
self.rows, self.columns = 1, 4
panel = wx.Panel(self)
self.grid = wx.grid.Grid(panel, style=wx.BORDER_THEME)
self.grid.CreateGrid(self.rows, self.columns)
self.grid.ShowScrollbars(wx.SHOW_SB_NEVER, wx.SHOW_SB_NEVER) # remove scroll bars
# set all cells in first row Read Only
attr = wx.grid.GridCellAttr()
attr.SetReadOnly(True)
self.grid.SetRowAttr(0, attr)
self.grid.HideRowLabels()
self.grid.SetRowSize(0, 20)
self.grid.DisableDragRowSize()
self.grid.SetColLabelSize(20)
self.grid.SetColLabelValue(0, 'Nintendo')
self.grid.SetColLabelValue(1, 'Super Nintendo')
self.grid.SetColLabelValue(2, 'Nintendo 64')
self.grid.SetColLabelValue(3, 'Nintendo GameCube')
v_box = wx.BoxSizer(wx.VERTICAL)
v_box.Add(self.grid, proportion=0, flag=wx.ALL | wx.EXPAND, border=10)
panel.SetSizer(v_box)
self.Layout()
# set the initial size of columns based on initial frame size
self.resize_with_frame()
self.grid.Bind(wx.EVT_SIZE, lambda event: self.resize_with_frame())
def resize_with_frame(self):
"""Dynamically resize columns with the size of the frame."""
width, height = self.GetClientSize()
for column in range(self.columns):
self.grid.SetColSize(column, (width - 20) / self.columns) # width - 20 for border of 10 on both sides
if __name__ == "__main__":
app = wx.App()
gui = MyGrid()
gui.Show()
app.MainLoop()
I'm making a table-like widget that displays an image, the file name, and two box-selection areas. I have two objects 'grid_row' & 'grid_table' (both using QGridLayout), grid_row being a single row and grid_table containing x number of grid_rows (I'm designing it like this because it's simply easier to keep track of my custom properties).
The tool looks like this
The final layout is a QVBoxLayout, then from top to bottom, I have QHBoxLayout(the one with a label and combobox), grid_row(for the headers 1,2,3), a scroll_area that contains the grid_table with each one being grid_rows. Lastly another QHBoxLayout for the buttons.
Each grid_row contains a 'image-widget', and two region labels(QLabel). The image widget contains a label(I used setPixmap for display) and a pushbutton. Here are my grid_row and image_widget classes:
class grid_row(QWidget):
def __init__(self, parent=None):
super().__init__()
#self.frame = frame_main()
self.grid_layout = QGridLayout()
self.grid_layout.setSpacing(50)
self.image_widget = image_widget()
self.grid_layout.addWidget(self.image_widget, 0, 0, 1, 1, Qt.AlignHCenter)
self.region_2 = QLabel('null')
self.grid_layout.addWidget(self.region_2, 0, 2, 1, 1, Qt.AlignHCenter)
self.setLayout(self.grid_layout)
self.region_1 = QLabel('null')
self.grid_layout.addWidget(self.region_1, 0, 1, 1, 1, Qt.AlignHCenter)
class image_widget(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.image_widget_layout = QHBoxLayout()
self.image_widget_label = QLabel()
self.image_widget_label.setPixmap(QPixmap('default.png').scaled(96, 54))
self.image_widget_layout.addWidget(self.image_widget_label)
self.img_btn = QPushButton()
self.img_btn.setEnabled(False)
self.img_btn.setText('Drag Here!')
self.image_widget_layout.addWidget(self.img_btn)
self.setLayout(self.image_widget_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
layout = QVBoxLayout()
grid_row = grid_row()
layout.addWidget(grid_row)
btn = QPushButton('press')
btn.clicked.connect(lambda: grid_row.region_1.setText('[0,0,1920,1080]'))
layout.addWidget(btn)
widget.setLayout(layout)
scroll_area = QScrollArea()
scroll_area.setWidget(widget)
scroll_area.show()
sys.exit(app.exec_())
So currently, I've implemented events that allow me to drag images into the image_widget and click the push button to modify the two regions that are framed (format: [x1, y1, x2, y2]). The problem is that when I do that(e.g. region values go from 'null' to say '[20,20, 500, 500]', the image gets squished because now the labels are taking up more width.
I realize that some size policy needs to be set (and maybe other properties) but I don't know which property to use and on which widget. I want the image to remain the same. Maybe stretch out the width of each column for the grid_row?
To clarify, I want the label containing the pixmap to remain the same size (always 96*54) and fully displayed(not cropped or stretched) at all times.
I've provided the a simplified executable code to display my problem, the classes are the same as my code, I just only put grid_row inside the scroll_area and added a button to change one of the values of the region to simulate the situation. Can provide additional code if needed. Thanks in advance!
Wow sometimes the answer is really one extra line of code...
So the documentation mentions that QScrollArea by default honors the size of its widget. Which is why when I changed the region (to a value that's wider/ more text) the widget does not auto adjust.
I needed to add
scroll_area.setWidgetResizable(True)
to allow the widget to resize wider thus prompting the scroll bars to appear. This way my pixmap image doesn't get cropped from not having enough space.
The easiest way would be to add size constraints to the label before adding to the layout
self.image_widget_label.adjustSize()
self.image_widget_label.setFixedSize(self.image_widget_label.size())
self.image_widget_layout.addWidget(self.image_widget_label)
adjustSize would resize the label depending on the contents.
The more difficult way is to answer the questions :
"when I change the size of the overall window, how do I want this
particular item to behave? When the window is at its minimal size,
which items do I want hidden or out of view? When the window is full
size, where do I want empty spots?"
To answer these better read a bit on Qt Layout management
In wxPython, I'm having trouble centering a sub-panel that I've created to have a fixed aspect ratio.
To control the aspect ratio, the sub-panel needs to capture the Size event and then do an explicit SetSize. So, I've done that and it works well. Unfortunately, when I embed this sub-panel into another panel (using a sizer), the wx.ALIGN_CENTER_HORIZONTAL flag doesn't work.
Apparently, the sizer tells my sub-panel that it has the whole width to fit within. When my window doesn't use the whole width, then the sizer doesn't adjust. Here is a simplified version of my code that shows the problem:
import wx
ROWS = 6
COLS = 25
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
label = wx.StaticText(self, -1, 'Label')
grid = TestGrid(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(label, 0, flag=wx.ALIGN_CENTER_HORIZONTAL)
sizer.Add(grid, 1, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND)
self.SetSizer(sizer)
class TestGrid(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
self.SetBackgroundColour(wx.BLUE)
self.Bind(wx.EVT_SIZE, self.OnSize)
def OnSize(self, event):
w, h = self.GetSizeTuple()
delta = min(w // COLS, h // ROWS)
self.SetSize((COLS*delta, ROWS*delta))
self.Refresh()
if __name__ == '__main__':
app = wx.App()
frame = wx.Frame(None, -1, "Test", size=(600, 200))
panel = TestPanel(frame)
frame.Show(True)
app.MainLoop()
This is a screen capture:
As VZ explained in his answer, your wxEVT_SIZE handler isn't a very good idea. Remove it. When using sizers, you have to work within the sizer infrastructure to achieve what you want.
You want your TestGrid to fill as much space as possible within its parent, while maintaining a certain aspect ratio; that's exactly what wx.SHAPED is for (combined with a proportion greater than 0, which you already have). So, you should add grid to the sizer like this:
sizer.Add(grid, 1, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.SHAPED)
The only other thing you need to do is tell the sizer what your desired aspect ratio is. The easiest way is to set an initial size for your TestGrid (before it's added to the sizer); for example, set the size of its wxPanel base:
wx.Panel.__init__(self, parent, -1, size=(COLS*7, ROWS*7))
That's all there is to it.
(I'm just guessing the Python syntax, so ignore any obvious errors :-) )
You can't, or at the very least shouldn't, resize a window from its own wxEVT_SIZE handler. I'm not sure what exactly are you trying to achieve, but if the goal is to encapsulate everything in TestGrid class, then you should let it have whichever size it has and create a nested window in it that you would resize to the appropriate size in your OnSize.
I have a set of images available. If I click on one of those images is there a way to determine which of the images has been clicked on in wxPython?
You will almost certainly have to calculate it for yourself. The most straight-forward method would be to use a mouse event like wx.EVT_LEFT_DOWN and grab the mouse's coordinates in the event handler. Then use that information to tell you where on your wxPython window you clicked. Each of your image widgets or DCs or whatever you're using can report it's size and position, so if the mouse coordinates are in X image's boundaries, you know it's been clicked on. You might also be able to use the HitTest() method, depending on what you're using to show the images.
EDIT: Here is how you would do it if you were using a wx.StaticBitmap, which actually lets you attach an wx.EVT_LEFT_DOWN to it:
import wx
class PhotoCtrl(wx.Frame):
def __init__(self):
size = (400,800)
wx.Frame.__init__(self, None, title='Photo Control', size=size)
self.panel = wx.Panel(self)
img = wx.EmptyImage(240,240)
self.imageCtrl = wx.StaticBitmap(self.panel, wx.ID_ANY,
wx.BitmapFromImage(img),
name="emptyImage")
imageCtrl2 = wx.StaticBitmap(self.panel, wx.ID_ANY,
wx.BitmapFromImage(img),
name="anotherEmptyImage")
self.imageCtrl.Bind(wx.EVT_LEFT_DOWN, self.onClick)
imageCtrl2.Bind(wx.EVT_LEFT_DOWN, self.onClick)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.imageCtrl, 0, wx.ALL, 5)
mainSizer.Add(imageCtrl2, 0, wx.ALL, 5)
self.panel.SetSizer(mainSizer)
self.Show()
#----------------------------------------------------------------------
def onClick(self, event):
""""""
print event.GetPosition()
imgCtrl = event.GetEventObject()
print imgCtrl.GetName()
if __name__ == '__main__':
app = wx.App(False)
frame = PhotoCtrl()
app.MainLoop()
you dont tell us anything about how you are displaying your images? are you blitting them right on the dc? are you creating panels for them? etc... properly setting up your project is important. basically you give us zero information to help you with.
Keeping all that in mind, something like this would work fine (this is called a self contained code example, you should always provide one with your questions, to make it easier for people to help you)
import wx
a = wx.App(redirect=False)
f= wx.Frame(None,-1,"Some Frame",size = (200,200))
sz = wx.BoxSizer(wx.HORIZONTAL)
def OnClick(evt):
print "Clicked:",evt.GetId()-10023
for i,img in enumerate(["img1","img2","img3"]):
id = 10023+i
p = wx.Panel(f,-1)
sz.Add(p)
sz1 = wx.BoxSizer()
p.Bind(wx.EVT_LEFT_UP,OnClick)
bmp = wx.Image(img).ConvertToBitmap()
b = wx.StaticBitmap(p,-1,bmp)
sz1.Add(b)
p.SetSizer(sz1)
f.SetSizer(sz)
f.Layout()
f.Fit()
f.Show()
a.MainLoop()
Keep in mind I didnt test it... but theoretically it should work...
So I've got an issue with Grid and Sizers in wxPython, if i include this table the formating seems to mess up somehow, the entire window appears to have the correct size and the items appear at the correct locations.
But when this table is included the inner light grey which I guess is the panel ?
but items below the table are hidden because of this dark grey field
removing the setcolsize calls doesn't fix it either
Thanks for any replies
wx.Frame.__init__(self, parent, title=title)
panel = wx.Panel(self)
grid = wx.GridBagSizer(hgap=5, vgap=5)
# some other items comes before this
# FileGrid
fileF = wx.grid.Grid(panel)
fileF.CreateGrid(2,3)
fileF.SetColLabelSize(0)
fileF.SetRowLabelSize(0)
fileF.SetCellValue(0, 0, "Old")
fileF.SetCellValue(0, 1, "New")
fileF.SetCellValue(0, 2, "Update?")
grid.Add(fileF, pos=(5,0))
# FileGrid end
runBtn = wx.Button(panel, wx.ID_APPLY, "Apply")
self.Bind(wx.EVT_BUTTON, self.applyScRen, runBtn)
grid.Add(runBtn, pos=(6,0))
self.SetSizerAndFit(grid)
You do not provide the full frame structure but this should get you on the right track.
Change the last line to:
panel.SetSizerAndFit(grid)
That already improves matters. The container for the sizer is the panel and that, in turn, is contained inside the frame.
EDIT
This pastebin has the full working version. It produces this image followed by the key change I made to your code:
#panel.CreateStatusBar()
vSizer.Add(grid, 0, wx.ALL, 5)
panel.SetSizerAndFit(grid)
self.CreateStatusBar()
self.SetInitialSize()
self.Show(True)