Set Max Width for Frame with ScrolledWindow in wxPython - python

I created a Frame object and I want to limit the width it can expand to. The only window in the frame is a ScrolledWindow object and that contains all other children. I have a lot of objects arranged with a BoxSizer oriented vertically so the ScrolledWindow object gets pretty tall. There is often a scrollbar to the right so you can scroll up and down.
The problem comes when I try to set a max size for the frame. I'm using the scrolled_window.GetBestSize() (or scrolled_window.GetEffectiveMinSize()) functions of ScrolledWindow, but they don't take into account the vertical scrollbar. I end up having a frame that's just a little too narrow and there's a horizontal scrollbar that will never go away.
Is there a method that will account compensate for the vertical scrollbar's width? If not, how would I get the scrollbar's width so I can manually add it to the my frame's max size?
Here's an example with a tall but narrow frame:
class TallFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title='Tall Frame')
self.scroll = wx.ScrolledWindow(parent=self) # our scroll area where we'll put everything
scroll_sizer = wx.BoxSizer(wx.VERTICAL)
# Fill the scroll area with something...
for i in xrange(10):
textbox = wx.StaticText(self.scroll, -1, "%d) Some random text" % i, size=(400, 100))
scroll_sizer.Add(textbox, 0, wx.EXPAND)
self.scroll.SetSizer(scroll_sizer)
self.scroll.Fit()
width, height = self.scroll.GetBestSize()
self.SetMaxSize((width, -1)) # Trying to limit the width of our frame
self.scroll.SetScrollbars(1, 1, width, height) # throwing up some scrollbars
If you create this frame you'll see that self.SetMaxSize is set too narrow. There will always be a horizontal scrollbar since self.scroll.GetBestSize() didn't account for the width of scrollbar.

This is a little ugly, but seems to work on Window and Linux. There is difference, though. The self.GetVirtualSize() seems to return different values on each platform. At any rate, I think this may help you.
width, height = self.scroll.GetBestSize()
width_2, height_2 = self.GetVirtualSize()
print width
print width_2
dx = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X)
print dx
self.SetMaxSize((width + (width - width_2) + dx, -1)) # Trying to limit the width of our frame
self.scroll.SetScrollbars(1, 1, width, height) # throwing up some scrollbars

Related

Tkinter - relation between font type and width

I apologize in advance if my question is a duplicate however I have not found an answer to this question.
I'm learning Tkinter and I'm struggling with understanding the relation between a label's font type, it's size and it's width and the length of the string in it.
Specifically, what my problem is:
I have created a widget: a 800x640 canvas on which I want to place other
widgets.
On this canvas I want to place a label with some text which has the following
attributes: font: Helvetica, font size: 20, text = "main application". I want
to place this label widget at the very most top left corner of the
widget(meaning at point 0,0 with respect to the canvas). I want the label to
be 200 in width meaning it's background to take almost 1/3 of the canvas's
size(after I manage to do this I plan to add 2 more labels as well). I guess
the height of the label is determined by the font size in this case 20. I
placed the label at coordinate y=20 and this coordinate seems to be ok.
I did some googling and found out that the width parameter of the label widget is not the actual width but something related to the font and size of the label's text: something like if I understood correctly: if the width is 6 than the label will be wide enough to contain 6 characters of, in my case verdana size 20. But I was not able to figure out what width and what x coordinate I should give my label so it starts at the x point of the canvas. Here is the code that I wrote:
from tkinter import *
from tkinter.ttk import *
from tkinter import messagebox
from tkinter import Menu
# Define the application class where we will implement our widgets
class Application(Frame):
def __init__(self, master):
super(Application, self).__init__(master)
# CANVAS COLOUR DEFAULTS TO THE COLOUR OF THE WORKING WINDOW
canvas = Canvas(master, width=800, height = 640, bg="gray") # IF YOU DO .PACK() HERE IT WILL RETURN NONE AND THEN YOU WILL HAVE PROBLEMS BECAUSE .PACK() RETURNS A 'NONE' TYPE OBJECT
canvas.place(relx=0.5, rely=0.5, anchor=CENTER)
# The 'menu' of the application. The selection labels
main_application_label = Label(master, text="main_application", font=("Helvetica", 20))
main_application_window = canvas.create_window(103,20, window=main_application_label)
main_application = Tk()
main_application.title("main_application")
app = Application(main_application)
app_width = 800
app_height = 640
screen_width = main_application.winfo_screenwidth()
screen_height = main_application.winfo_screenheight()
x_coord = (screen_width/2) - (app_width/2)
y_coord = (screen_height/2) - (app_height/2)
main_application.geometry("%dx%d+%d+%d" % (app_width, app_height, x_coord, y_coord))
main_application.mainloop()
I have managed to somehow get the label at around point 0,0(by giving more values till I got it right) but the actual width of the label is not 200 pixels(~1/3 of the canvas). Please help me understand what values to the width parameter I should give so that my label's background is 1/3 of the canvas's size and if possible explain the relation between character font and width parameter so I can do that for any widgets regardless of their text's length. Thank you for reading my post!
Edit: What I wanted to do was to place 3 widgets(labels in this case but it doesn't matter) on the canvas. I did not understand what the 'anchor' option does and that was confusing me because I was expecting the center of the widget to be placed at the given coordinates all times but as I was changing anchor the placement of the center of the widget was changing and that was confusing me. It's all clear now thanks to #Bryan Oakley. Thanks.
If you want the upper left corner of the text to be at (0,0), you don't have to adjust the coordinates based on the width. You can use the anchor option when creating the canvas object:
main_application_window = canvas.create_window(0, 0, anchor="nw",
window=main_application_label)
If you really need to compute the actual size of the string, you can create a Font object and then use the measure method to find the actual width of a string in the given font.
from tkinter.font import Font
font = Font(family="Helvetica", size=20)
string_width = font.measure("main_application")
string_height = font.metrics("linespace")
This gives you the size of the rendered string. If you're using a label widget you'll also need to take into account the amount of padding and borders that the widget uses.
When you create items on a canvas, you can specify the width and height. For example, this makes the widget 200 pixels wide:
main_application_window = canvas.create_window(0, 0, anchor="nw", width=200,
window=main_application_label, width=400)

Issues with canvas scrollbars in Tkinter

Relevant code:
self.propertyListWrapper = ttk.Frame(self.propertyMenu)
self.propertyListWrapper.pack( fill = tk.BOTH, expand = tk.YES )
self.propertyListCanvas = tk.Canvas(self.propertyListWrapper)
self.propertyListCanvas.pack( fill = tk.BOTH, expand = tk.YES, side = tk.LEFT )
self.propertyGrid = ttk.Frame(self.propertyListCanvas)
self.propertyListScrollbar = ttk.Scrollbar(self.propertyListWrapper)
self.propertyListScrollbar.config(command = self.propertyListCanvas.yview)
self.propertyListCanvas.config(yscrollcommand = self.propertyListScrollbar.set)
self.propertyListScrollbar.pack(side = tk.RIGHT, fill = tk.Y)
...
# where stuff is added to self.propertyGrid
...
self.propertyListCanvas.config( scrollregion = (0, 0, self.propertyGrid.winfo_width(), self.propertyGrid.winfo_height()))
self.propertyListCanvas.create_window((0,0), window = self.propertyGrid, anchor='nw')
This is what is currently happening:
As you can see, the scrollbar doesn't have a bar. Not very useful.
What am I doing wrong?
Here's a simplified Github Gist that duplicates the problem. 20 labels should be there, and you should be able to scroll through them, but the scrollbar doesn't appear.
The default for pack is to put something along the top edge. When you packed the canvas you're using the default, so it takes up the top of the gui, leaving blank space below. When you packed the scrollbar, you told it to go to the right, so it is at the right edge of the empty space below the canvas. If you want the scrollbar and canvas side-by-side, pack both on the right, or pack one on the right and one on the left.
The problem with your gist is that you're trying to get the width and height of the frame before it has been drawn. Since the frame hasn't been mapped to the screen (because you haven't added it to the canvas yet) it's width and height is 1 (one). You must make a widget visible before you can measure its width and height. That means to add it to a canvas or pack/place/grid it to a visible window and then wait for a screen repaint.

How to set canvas size properly in Tkinter?

I am using Tkinter to visualize my data points. My problem is that I cannot make the data points appear in the center of the canvas and meanwhile the canvas is big enough.
To make the canvas look good, I wish to fix it at around 800*600 (I think the unit is pixel). So I did the following:
class DisplayParticles(Canvas):
def __init__(self):
# Canvas
Canvas.__init__(self)
self.configure(width=800, height=600)
# Particles
self.particle_radius = 1
self.particle_color = 'red'
# User
self.user_radius = 4
self.user_color = 'blue'
self.ghost_color = None
However, my data to be plotted are in the unit of meter. Plus, they center around the origin (0, 0), which means that there are negative coordinates for both x and y.
Then when I plot them on the canvas I get something like this
Obviously, the data points were plotted in pixel!
I wish the canvas to be big enough on the screen and meanwhile the data are plotted in a proper scale centered at the canvas. (Place my origin (0, 0) at the canvas center)
How may I do that?
The visible center of the canvas can be changed by setting the scrollregion attribute. For example, if you set the scrollregion to (-400,-400, 400, 400) in a canvas that is 800 pixels wide, the coordinate 0,0 will appear in the center of the screen.
Here's an example that draws a square at 0,0, and which appears in the center of the screen:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self,*args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.canvas = tk.Canvas(width=800, height=800)
self.canvas.pack(side="top", fill="both", expand=True)
self.canvas.configure(scrollregion=(-400, -400, 400, 400))
self.canvas.create_rectangle(-10,-10,10,10, fill="red", outline="black")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
You can also use the xview_scroll and yview_scroll methods to move 0,0 into the center of the visible portion of the canvas. Instead of setting the scrollregion as in the above example, you can do this after creating your canvas:
self.canvas.xview_scroll(800, "units")
self.canvas.yview_scroll(800, "units")
This will programmatically scroll the canvas over and down 800 pixels, so that 0,0 is in the center of the screen.
Use the xview and yview methods to scroll the canvas view so the origin is in the center.
Scaling is not supported, so if you need that, you need to transform the source data, like Brionius suggests.
Your Canvas is not going to automatically scale to fit what you've drawn - you'll have to figure out what the proper size is and set it.
Also, Canvas coordinates will always start with (0, 0) in the upper left corner - no way to change that. That means you'll have to translate all the points you plot on the canvas. Luckily, that's easy:
width = ... # Determine the correct width
height = ... # Determine the correct height
self.configure(width=width, height=height)
coords = (-20, -30, 10, 60) # The coordinates of a shape you want to draw
# add width/2 and height/2 to x and y coordinates respectively so that the (0, 0) coordinate is shifted to the center of the canvas:
newCoords = (coords[0]+width/2, coords[1]+height/2, coords[2]+width/2, coords[3]+height/2)
self.create_oval(*newCoords) # Create the translated shape

change size of Frame even if there widget python

hi is there any way to change width and height of widget even if there's widget?
i have code like this
form = Tk()
form.geometry("500x500")
def click():
global frame
frame.config(height = 0 ,width = 0)
frame = LabelFrame(form , text = "vaaja")
frame.place(x = 20 , y = 30)
Label(frame, text ="1").grid(row = 0,column = 0 )
Label(frame, text = "2").grid(row = 1 ,column = 0 )
Button(form , text="Click", command = click).place(x = 200 , y = 200)
form.mainloop()
and when I click the button the size of the frame is the same ( I'cant use grid_forget() for labels and then change the size of frame)
Because you are using place, you have two solutions: you can use place to set the width and height to zero, or you can turn geometry propagation off.
Using place to set the width and height
place allows you to define the width and the height of the placed widget, so in your click function you can do this:
def click():
frame.place_configure(width=0, height=0)
Turning geometry propagation off
A frame is resized to fit its contents by something called "geometry propagation". If you turn this off, you can control the size of the frame with the width and height options of the frame itself. Usually it's better to let Tkinter decide the size for you, but sometimes there's a need to have an explicit size, which is why it's possible to turn geometry propagation off.
Since you are using grid to manage the widgets internal to the frame, you need to use grid_propagate(False) to turn geometry propagation off for that frame:
frame.grid_propagate(False)
By doing so, you're responsible for setting the initial width and height of the widget, though you could leave propagation on to get the initial size, then turn it off with the button click in order to work around that issue.
There's an interesting bug (or feature...) in that if you set the width and height to zero, Tkinter won't redraw the window. At least, it doesn't on the Mac. I don't recall the workaround for that because I never, ever need to set a widget to a zero size, but setting it to 1x1 pixel makes it nearly invisible.

wxPython GridSizer not attached to panel?

I'm trying to build a level editor for a game I'm working on. It pulls data from a flat file and then based on a byte-by-byte search it'll assemble a grid from pre-set tiles. This part of the app I should have no issues with. The problem is that my test version of the editor which just loads a 16x16 grid of test tiles from 00 to FF is loading in the wrong place.
Example: The app frame looks like this:
|-T-------|
| | |
| | |
| | |
| | |
|---------|
Excusing my horrible ASCII art, the frame essentially has a horizontal sizer on it, with 2 vertical sizers, one for the left and one for the right. Each of these has a panel in it, which each have a second sizer in them. The left sizer has a 64 pixel-wide spacer in it, then a gridsizer which is later filled with images. The right second sizer is user sizable but a minimum of 960 pixels via a spacer there, then a gridsizer that's determined by the level width and height in code.
Essentially, for each side - there's a gridsizer inside a sizer which has a spacer for the width of the section, which are on a panel that's inside a sizer for that half of the sizer that's on the main frame. I hope this makes sense, as it confuses me at times :P
Here's the code that does all this:
#Horizontal sizer
self.h_sizer = wx.BoxSizer(wx.HORIZONTAL)
#Vertical sizer
self.v_sizer_left = wx.BoxSizer(wx.VERTICAL)
self.v_sizer_right = wx.BoxSizer(wx.VERTICAL)
#Create the 2 panels
self.leftPanel = wx.ScrolledWindow(self, style = wx.VSCROLL | wx.ALWAYS_SHOW_SB)
self.leftPanel.EnableScrolling(False, True)
self.rightPanel = wx.ScrolledWindow(self, style = wx.VSCROLL | wx.ALWAYS_SHOW_SB)
self.rightPanel.EnableScrolling(False, True)
#Create a sizer for the contents of the left panel
self.lps = wx.BoxSizer(wx.VERTICAL)
self.lps.Add((64, 0)) #Add a spacer into the sizer to force it to be 64px wide
self.leftPanelSizer = wx.GridSizer(256, 1, 2, 2) # horizontal rows, vertical rows, vgap, hgap
self.lps.Add(self.leftPanelSizer) #Add the tiles grid to the left panel sizer
self.leftPanel.SetSizerAndFit(self.lps) #Set the leftPanel to use LeftPanelSizer (it's not lupus) as the sizer
self.leftPanel.SetScrollbars(0,66,0, 0) #Add the scrollbar, increment in 64 pixel bits plus the 2 spacer pixels
self.leftPanel.SetAutoLayout(True)
self.lps.Fit(self.leftPanel)
#Create a sizer for the contents of the right panel
self.rps = wx.BoxSizer(wx.VERTICAL)
self.rps.Add((960, 0)) #Set it's width and height to be the window's, for now, with a spacer
self.rightPanelSizer = wx.GridSizer(16, 16, 0, 0) # horizontal rows, vertical rows, vgap, hgap
self.rps.Add(self.rightPanelSizer) #Add the level grid to the right panel sizer
self.rightPanel.SetSizerAndFit(self.rps) #Set the rightPanel to use RightPanelSizer as the sizer
self.rightPanel.SetScrollbars(64,64,0, 0) #Add the scrollbar, increment in 64 pixel bits - vertical and horizontal
self.rightPanel.SetAutoLayout(True)
self.rps.Fit(self.rightPanel)
#Debugging purposes. Colours :)
self.leftPanel.SetBackgroundColour((0,255,0))
self.rightPanel.SetBackgroundColour((0,128,128))
#Add the left panel to the left vertical sizer, tell it to resize with the window (0 does not resize, 1 does). Do not expand the sizer on this side.
self.v_sizer_left.Add(self.leftPanel, 1)
#Add the right panel to the right vertical sizer, tell it to resize with the window (0 does not resize, 1 does) Expand the sizer to fit as much as possible on this side.
self.v_sizer_right.Add(self.rightPanel, 1, wx.EXPAND)
#Now add the 2 vertical sizers to the horizontal sizer.
self.h_sizer.Add(self.v_sizer_left, 0, wx.EXPAND) #First the left one...
self.h_sizer.Add((0,704)) #Add in a spacer between the two to get the vertical window size correct...
self.h_sizer.Add(self.v_sizer_right, 1, wx.EXPAND) #And finally the right hand frame.
After getting all the data, the app will then use it to generate the level layout with .png tiles from a specified directory but for testing purposes I'm just generating a 16x16 grid from 00 to FF, as mentioned above - via a menu option I call this method:
def populateLevelGrid(self, id):
#This debug method fills the level grid scrollbar with 256 example image tiles
levelTileset = self.levelTilesetLookup[id]
#levelWidth = self.levelWidthLookup[id]
#levelHeight = self.levelHeightLookup[id]
#levelTileTotal = levelWidth * levelHeight
levelTileTotal = 256 #debug line
self.imgPanelGrid = []
for i in range(levelTileTotal):
self.imgPanelGrid.append(ImgPanel.ImgPanel(self, False))
self.rightPanelSizer.Add(self.imgPanelGrid[i])
self.imgPanelGrid[i].set_image("tiles/"+ levelTileset + "/" + str(i) + ".png")
self.rightPanelSizer.Layout()
self.rightPanelSizer.FitInside(self.rightPanel)
This works, but pins the grid to the top left of the entire frame, not to the top left of the right half of the frame - that it should be on. There's similar code to do a 1x256 grid on the left frame but I've no way of telling if that's suffering the same issue for obvious reasons. Both have working scrollbars but have redrawing issues when scrolled, making me wonder if it's drawing the images over the entire application and just ignoring the application layout.
Is there something I've missed here? This is the first time I've done anything with gridsizers, images and GUIs in general in python, having only recently started with the language after wanting to write in something a little more cross-platform than VB6 - any thoughts? =/
That's a lot of code and a lot of sizers! But I think maybe your ImgPanels should have the rightPanel as their parent, and not self.
Also, do you ever call self.SetSizer(self.h_sizer)? Didn't see that anywhere.
I recommend creating portions of the layout in separate functions. Then you don't have to worry about your local namespace being polluted with all these wacky sizer names. So for each part of the sizer hierarchy you could have a function.
create_controls
create_left_panel
create_grid
create_right_panel
Also, I usually use SetMinSize on the child controls instead of adding dummy spacers to the sizers to setup size constraints. Then Fit will do it for you. Speaking of Fit, I didn't even know it could take arguments!

Categories

Resources