SZIEBERTH Ádám
SZIEBERTH Ádám

Reputation: 4206

Tkinter PanedWindow.sash_place() fails

I wanted to subclass PanedWindow widget in a way it can shrink/expand its panes proportionally. However it seems I can't place sashes to coordinates greater than reqsize of the widget. Demo:

from tkinter import *

class VerticalPropPanedWindow(PanedWindow):

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, orient="vertical",
            **kwargs)
        self._default_weights, self._saved_weights = [], []

        self.bind("<Button-3>", self.reset)
        self.bind("<ButtonRelease-1>", self.save_weights)
        self.bind("<Configure>", self.changed)

    def add(self, child, weight, **options):
        self._default_weights.append(weight)
        self._saved_weights.append(weight)
        super().add(child, **options)

    def align(self):
        wsize = self.winfo_height()
        print("height: {}, reqheight: {}".format(wsize,
            self.winfo_reqheight()))
        sumw, coords = sum(self._saved_weights), []
        for w in self._saved_weights[:-1]:
            coords.append(sum(coords[-1:]) + int(wsize * w / sumw))
        print("aligning to: ", coords)
        for i, c in enumerate(coords):
            self.sash_place(i, 1, c)
        print("after align: ", [self.sash_coord(i)[1]
            for i in range(len(self.panes()) - 1)])

    def changed(self, event):
        self.align()

    def reset(self, event):
        self._saved_weights = self._default_weights
        self.align()

    def save_weights(self, event):
        n = len(self.panes()) - 1
        wsize, coords = self.winfo_height(), []
        for i in range(n):
            coords.append(self.sash_coord(i)[1] - sum(coords))
        self._saved_weights = coords + [wsize - sum(coords)]

if __name__ == "__main__":
    root = Tk()
    root.p = VerticalPropPanedWindow(root, bg="black")
    root.p.add(Label(root.p, text="1/5"), 1, sticky="nesw")
    root.p.add(Label(root.p, text="3/5"), 3, sticky="nesw")
    root.p.add(Label(root.p, text="1/5"), 1, sticky="nesw")
    root.p.pack(expand=1, fill='both')
    root.mainloop()

Try to resize the window to experience the strange behavior. By checking prints on the console you can see how aligning fails on the second coordinate if reqheight is not great enough.

However, by manually drag a pane abit and rightclicking on it thereafter (which resets the original distribution) works.

I see two solutions here:

  1. To force widget reqsize to actual size, but how?
  2. To find some hacky way to drag pane first as it would be done by the user. How?

Cheers, Ádám

Note: It works well with only two panes.

Edit: In align(): sum(coords) -> sum(coords[-1:])

Upvotes: 0

Views: 1944

Answers (1)

Bryan Oakley
Bryan Oakley

Reputation: 386352

Instead of subclassing PanedWindow, you might want to consider subclassing Frame, and using place to add children to the frame. Place excels at placing widgets at relative locations and with relative heights. With this, you don't have to do any shenanigans while the window is being resized. This is how we created paned windows before the PanedWindow widget was added to tk.

The downside, of course, is that you have to write the code to draw and respond to sash events. I'm not sure which is more work, but dealing with the sash is pretty straight-forward -- just include a one- or two-pixel frame between each panel, and set a binding to adjust the heights of the frame above and below it.

Here's a really quick hack that does the placement of the subframes. It does not handle the sashes, but it does leave a two-pixel tall area between each pane where you could put a sash.

import Tkinter as tk

class CustomPanedWindow(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        self.panes = []

    def add(self, widget, weight=1):
        '''Add a pane with the given weight'''
        self.panes.append({"widget": widget, "weight": weight})
        self.layout()

    def layout(self):
        for child in self.place_slaves():
            child.place_forget()

        total_weight = sum([pane["weight"] for pane in self.panes])
        rely= 0

        for i, pane in enumerate(self.panes):
            relheight = pane["weight"]/float(total_weight)
            # Note: relative and absolute heights are additive; thus, for 
            # something like 'relheight=.5, height=-1`, that means it's half
            # the height of its parent, minus one pixel. 
            if i == 0:
                pane["widget"].place(x=0, y=0, relheight=relheight, relwidth=1.0)
            else:
                # every pane except the first needs some extra space
                # to simulate a sash
                pane["widget"].place(x=0, rely=rely, relheight=relheight, relwidth=1.0, 
                                     height=-2, y=2)
            rely = rely + relheight

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        paned = CustomPanedWindow(self)
        paned.pack(side="top", fill="both", expand=True)

        f1 = tk.Frame(self, background="red", width=200, height=200)
        f2 = tk.Frame(self, background="green", width=200, height=200)
        f3 = tk.Frame(self, background="blue", width=200, height=200)

        paned.add(f1, 1)
        paned.add(f2, 2)
        paned.add(f3, 4)

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.geometry("400x900")
    root.mainloop()

Upvotes: 2

Related Questions