Templating overflowing content with glade and pygt

2019-09-18 08:53发布

问题:

I'm trying to develop a "multitrack" GUI (similar to multitrack audio editors); however, I'd like to develop it in glade first, and check how overflowing content (in this case, multiple "tracks") will behave with scollbars. Then, upon instantiation in Python, I'd first like to take the first of these "multiple tracks" as a "template", then delete all these multiple "tracks" - then allow the user to add new ones based on the "template" by, say, clicking an "Add" button.

From the Gtk palette, it seems to me that handlebox is the right object to use as a base for a "track" (I'd want to draw in these tracks eventually). The only thing I managed to get through so far (given how few tutorials can be found on glade UI usage), is to get the scrollbars to behave within the GUI - here's a screenshot of the scrolled window section only (corresponding file is below):

The right structure seems to be:

scrolled window
  viewport
    vbox
      handlebox 
        drawingarea
      handlebox ...

... and all I have to do is set the "Height request" of (all) handlebox to 150px (I want a constant height, and width scaling according to window); and set its Packing/Expand to "No". Also, set the scrolledwindow Horizontal and Vertical Scrollbar Policy to "Always" - otherwise the scrollbars are not shown (and I was otherwise wrongly trying to place an additional scrollbar to get to see it). Finally, to get the scrollbar to work, click exactly on its arrowheads - dragging the scroll bar doesn't work from within Glade (at least not on glade3 3.8.0 on Ubuntu 11.04 I use).

So far so good - at least I can see the overflowing content behave as I want in glade, but:

  • Is this the right glade UI structure to use? I see a Layout object, and a Frame object too - would those maybe be more appropriate here? (tried them, couldn't really figure them out)
  • Once the .glade file is read in Python, how to a proceed in "extracting" a template from handlebox1, and duplicating it on demand?
  • Would I also have to change the partitioning of the vbox upon add/delete of a track? If so, is there a way to achieve the same layout as above for adding/deleting tracks, without using a vbox?
  • Currently I'm happy with width of the tracks scaling with width of the window; but should I decide I want them fixed width bigger than width of the window, I tried setting Width Request of the handlebox to say 1000, and horizontal scrollbar seems to work properly in Glade; would Width Request be all that there is to it?
  • Are special handlers needed if I want to let the user rearrange vertical track order by dragging?

And a side question - is there a way to quickly "preview" a Glade object directly from Glade (just in an "empty window"), without writing an instantiation script - maybe by using some shortcut?

Here's the code of multitrack.glade (in GtkBuilder):

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <requires lib="gtk+" version="2.24"/>
  <!-- interface-naming-policy project-wide -->
  <object class="GtkWindow" id="window1">
    <property name="can_focus">False</property>
    <child>
      <object class="GtkScrolledWindow" id="scrolledwindow1">
        <property name="visible">True</property>
        <property name="can_focus">True</property>
        <child>
          <object class="GtkViewport" id="viewport1">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <object class="GtkVBox" id="vbox1">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <child>
                  <object class="GtkHandleBox" id="handlebox1">
                    <property name="height_request">150</property>
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <object class="GtkDrawingArea" id="drawingarea1">
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="fill">True</property>
                    <property name="position">0</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkHandleBox" id="handlebox2">
                    <property name="height_request">150</property>
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <placeholder/>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="fill">True</property>
                    <property name="position">1</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkHandleBox" id="handlebox3">
                    <property name="height_request">150</property>
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <placeholder/>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="fill">True</property>
                    <property name="position">2</property>
                  </packing>
                </child>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

回答1:

Boy, this was something... well, to do a proper templating programaticaly, you'd need to recursively copy Gtk Objects, which are not susceptible to deepcopy... So I wrote one such function, deep_clone_widget, included in the source below; for reference:

  • Is there a good way to copy a Gtk widget?
  • How to use the same widget twice in pygtk?
  • How do I iterate through all Gtk children in PyGtk recursively?
  • Editing GtkWidget attributes/properties
  • How to set default style for widgets in pyGTK?

Of course, not extensively tested, but seems to work for me. Interestengly, until one does a full "deep clone" of handlebox and drawing area, the handleboxes do not stretch to fit the width of the window!

Good thing is - can just add to Vbox, no need to manage it; but it seems the dragging behavior will be a challenge... But I'd still like to know if this is the right Gtk/Glade UI hierarchy to use (and if there is a shortcut to preview from Glade)

The code below will also output the starting hierarchy:

window1 :: GtkWindow
-scrolledwindow1 :: GtkScrolledWindow
--viewport1 :: GtkViewport
---vbox1 :: GtkVBox
----handlebox1 :: GtkHandleBox
-----drawingarea1 :: GtkDrawingArea
----handlebox2 :: GtkHandleBox
----handlebox3 :: GtkHandleBox

... and the ending hierarchy:

window1 :: GtkWindow
-scrolledwindow1 :: GtkScrolledWindow
--viewport1 :: GtkViewport
---vbox1 :: GtkVBox
----hb1_template :: GtkHandleBox
-----drawingarea1 :: GtkDrawingArea
----hb1_template :: GtkHandleBox
-----drawingarea1 :: GtkDrawingArea
----hb1_template :: GtkHandleBox
-----drawingarea1 :: GtkDrawingArea
----hb1_template :: GtkHandleBox
-----drawingarea1 :: GtkDrawingArea

... hopefully confirming that the deep clone code is ok.

Here's the code multitrack.py, that uses the multitrack.glade above:

# needs gtk-builder (not for libglade)
import pygtk
pygtk.require("2.0")
import gtk
import copy
from pprint import pprint
import inspect


def main():
  global window
  gladefile = "/tmp/multitrack.glade"
  wTree = gtk.Builder()
  wTree.add_from_file(gladefile)
  window = wTree.get_object("window1")
  if not(window): return

  print "E: " + str( get_descendant(window, "nofind", level=0, doPrint=True) )
  doCopy()
  print "E: " + str( get_descendant(window, "nofind", level=0, doPrint=True) )

  window.connect("destroy", gtk.main_quit)
  window.set_size_request(600, 300)
  window.show_all() # must have!
  gtk.main()


def doCopy():
  global window
  # get template object
  hb1_ref = get_descendant(window, "handlebox1", level=0, doPrint=False)
  #hb1_template = copy.deepcopy(hb1_ref) # GObject non-copyable
  hb1_template = deep_clone_widget(hb1_ref)
  gtk.Buildable.set_name(hb1_template, "hb1_template")
  # get the container to be cleared
  vb1 = get_descendant(window, "vbox1", level=0, doPrint=False)
  # delete pre-existing in vbox (incl. hb1_ref)
  for i in vb1.get_children():
    vb1.remove(i)
  # create new content
  hb1_a = deep_clone_widget(hb1_template)
  vb1.pack_start(hb1_a, expand=False, fill=True, padding=0)
  hb1_b = deep_clone_widget(hb1_template)
  vb1.pack_start(hb1_b, expand=False, fill=True, padding=0)
  hb1_c = deep_clone_widget(hb1_template)
  vb1.pack_start(hb1_c, expand=False, fill=True, padding=0)
  hb1_d = deep_clone_widget(hb1_template)
  vb1.pack_start(hb1_d, expand=False, fill=True, padding=0)
  if 0: #small deep_clone_test
    print ".....>"
    vb1_ref = get_descendant(window, "vbox1", level=0, doPrint=False)
    vb1_copy = deep_clone_widget(vb1_ref)
    print "EEEEE "+ str( get_descendant(vb1_copy, "nofind", level=0, doPrint=True) )
    print ".....<"


# https://stackoverflow.com/questions/20461464/how-do-i-iterate-through-all-gtk-children-in-pygtk-recursively
def get_descendant(widget, child_name, level, doPrint=False):
  if widget is not None:
    if doPrint: print("-"*level + str(gtk.Buildable.get_name(widget)) + " :: " + widget.get_name())
  else:
    if doPrint:  print("-"*level + "None")
    return None
  if(gtk.Buildable.get_name(widget) == child_name):
    return widget;
  if (hasattr(widget, 'get_child') and callable(getattr(widget, 'get_child')) and child_name != ""):
    child = widget.get_child()
    if child is not None:
      return get_descendant(child, child_name,level+1,doPrint)
  elif (hasattr(widget, 'get_children') and callable(getattr(widget, 'get_children')) and child_name !=""):
    children = widget.get_children()
    found = None
    for child in children:
      if child is not None:
        found = get_descendant(child, child_name,level+1,doPrint)
        if found: return found

def deep_clone_widget(widget, inparent=None):
  dbg = 0
  widget2 = clone_widget(widget)
  if inparent is None: inparent = widget2
  if (hasattr(widget, 'get_child') and callable(getattr(widget, 'get_child'))):
    child = widget.get_child()
    if child is not None:
      if dbg: print "A1 inp", inparent.get_name(), "w2", widget2.get_name()
      childclone = deep_clone_widget(child, widget2)
      if dbg: print "A2", childclone.get_name()
      widget2.add( childclone )
      #return inparent
  elif (hasattr(widget, 'get_children') and callable(getattr(widget, 'get_children')) ):
    children = widget.get_children()
    for child in children:
      if child is not None:
        if dbg: print "B1 inp", inparent.get_name(), "w2", widget2.get_name()
        childclone = deep_clone_widget(child, widget2)
        if dbg: print "B2", childclone.get_name()
        inparent.add( childclone )
        #return childclone
  return widget2

# https://stackoverflow.com/questions/1321655/how-to-use-the-same-widget-twice-in-pygtk
def clone_widget(widget):
  print(" > clone_widget in: " + str(gtk.Buildable.get_name(widget)) + " :: " + widget.get_name() )
  widget2=widget.__class__()
  # these must go first, else they override set_name from next stage
  for pspec in widget.props:
    if pspec.name not in ['window', 'child', 'composite-child', 'child-detached', 'parent']:
      #print("  > " + pspec.name)
      try:
        widget2.set_property(pspec.name, widget.get_property(pspec.name))
      except Exception as e:
        print e
  # here set_name is obtained
  for prop in dir(widget):
    if prop.startswith("set_") and prop not in ["set_buffer"]:
      #print("  ! " + prop + " ")
      prop_value=None
      try:
        prop_value=getattr(widget, prop.replace("set_","get_") )()
      except:
        try:
          prop_value=getattr(widget, prop.replace("set_","") )
        except:
          continue
      if prop_value == None:
        continue
      try:
        #print("  > " + prop + " " + prop_value )
        if prop != "set_parent": # else pack_start complains: assertion `child->parent == NULL' failed
          getattr(widget2, prop)( prop_value )
      except:
        pass
  gtk.Buildable.set_name(widget2, gtk.Buildable.get_name(widget))
  ## style copy:
  #for pspec in gtk.widget_class_list_style_properties(widget):
  #  print pspec, widget.style_get_property(pspec.name)
  #  #gtk.widget_class_install_style_property(widget2, pspec) #nope, for class only, not instances!
  # none of these below seem to change anything - still getting a raw X11 look after them:
  widget2.ensure_style()
  widget2.set_style(widget.get_style().copy())
  widget2.set_style(gtk.widget_get_default_style())
  widget2.modify_style(widget.get_modifier_style())
  #widget2.set_default_style(widget.get_default_style().copy()) # noexist; evt. deprecated? https://stackoverflow.com/questions/19740162/how-to-set-default-style-for-widgets-in-pygtk
  # this is the right one, so we don't get raw X11 look:
  widget2.set_style(widget.rc_get_style())
  return widget2

if __name__ == "__main__":
  main()