Creating Qt Apps That Run In A Tab

84 views
Skip to first unread message

Thomas Passin

unread,
Apr 9, 2023, 12:31:03 AM4/9/23
to leo-editor
Many Qt programs follow the same pattern.  There is a top-level QWidget that contains all the child widgets used by the program, and initializes all the variables and controls.  This widget is embedded in a QMainWindow (or your own subclass of one) by making it the window's "central widget".  Then your app's QApplication instantiates the main window and starts the Qt event loop.

You can make that same top-level QWidget run in a tab in Leo's log frame by embedding it in a new tab.  Your app can have full access to Leo's objects and services via the g and c variables.  So you can manipulate the tree and node bodies, for example.

This post includes an outline demonstrating how to do this, but in practice the  actual app (that is, the actual QWidget subclass that makes up the app) is up to you.

Leo is a much more complicated program that does a lot of critical startup tasks outside of Qt, so I don't think we can get Leo to run in one of its own tabs [!], fun though that might be.

There are three parts to these "tabbed" apps:

1. Creating the top-level QWidget;
2. Inserting it into a tab and controlling the tab (e.g., toggling it on and off);
3. Interacting with the app.

#1 is outside the scope of what I want to write about here, since it is completely dependent on the kind of program you want.  The attached demo plots curves with MatPlotLib and PlotNine, but it is only an example.

#2 is the key part of this technique, and will be covered in the next post.

#3 will be covered in a follow-up post.  There are some interesting tricks that you can do so that you can change certain parameters and even methods without having to edit you main QWidget code.  This is useful for development and sometimes for interactive use.

For now, check out the demo.  It is a Leo outline that can plot curves using either MatPlotLib or PlotNine.  Open the outline in Leo.  Notice that there is a button in the icon bar labeled "pyplot".  Click this button.  A new tab named "Pyplot" will open in the Log frame.

I suggest that you adjust the layout of the panes to look like the attached screen shot.

There is a button on the bottom labeled "Plot".  Click it.  A curve will be displayed.  Click it several more times and see that different curves of different colors get plotted.  These plots are plotted with MatPlotLib.

In the outline there is a node named "Plot9 Monkeypatch tabbed Plot Command".  It contains a different data set and a different plotting technique - it uses PlotNine, which is a Python port of the famous R program ggplot. Select that node and then run it with CTRL-b.  This changes the effect of the "Plot" button so it runs the PlotNine code instead of the original code.  Click the "Plot" button and see the difference in the plot.  It shows different data and is displayed in a different style.

There is another similar node with the name "Pyplot Monkeypatch tabbed Plot Command".  This changes the effect of the "Plot" button to plot a different data set with MatPlotLib, but with a different style from the original version.  Select it and run it with CTRL-b.  Click the "Plot" button.  You will see the plot change.

Now click the "pyplot" button in Leo's iconbar.  See how the "Pyplot" tab vanishes.  Click it again.  It will re-appear.  Click its "Plot" button.  You will see it is bock to plotting the original data.  This is because the original code was re-initialized when the app tab was toggled on again.  It is also possible to remove the tab but keep its widget alive so that when the tab is restored it remembers its state.

That's all for this post.
tabbed-pyplot-demo-layout.png
tabbed-pyplot-demo.leo

Edward K. Ream

unread,
Apr 9, 2023, 5:17:47 AM4/9/23
to leo-e...@googlegroups.com
On Sat, Apr 8, 2023 at 11:31 PM Thomas Passin <tbp1...@gmail.com> wrote:
Many Qt programs follow the same pattern.

Many thanks for this post. Please create an info item using this post as pre-writing.

Edward

Thomas Passin

unread,
Apr 9, 2023, 8:32:24 AM4/9/23
to leo-editor
Remind me what you mean by an "info item".  Would this be a GitHub issue with a "devInfo" or "Info" tag?

Edward K. Ream

unread,
Apr 9, 2023, 1:14:13 PM4/9/23
to leo-e...@googlegroups.com
On Sun, Apr 9, 2023 at 7:32 AM Thomas Passin <tbp1...@gmail.com> wrote:
Remind me what you mean by an "info item".  Would this be a GitHub issue with a "devInfo" or "Info" tag?

Exactly.

Edward

Thomas Passin

unread,
Apr 13, 2023, 9:27:39 AM4/13/23
to leo-editor
I have re-organized the code a bit to make to easier to read.  It does the same job  as before.  Also, I have zipped up the outline to make it easier to get from the browser.

More to come soon.

tabbed-pyplot-demo.zip
Message has been deleted

Thomas Passin

unread,
Apr 13, 2023, 12:08:21 PM4/13/23
to leo-editor
In my first post on apps in tabs, I wrote this:


There are three parts to these "tabbed" apps:

1. Creating the top-level QWidget;
2. Inserting it into a tab and controlling the tab (e.g., toggling it on and off);
3. Interacting with the app.

Instead of "Creating the top-level Widget" I could better have written "Designing the top-level widget".  Item 2. is the heart of the tabbed-app approach, and that's what this post is about.

To make it work, we have to:

1. Find the log frame;
2. Create a new tab;
3. Insert our top-level QWidget into it;
4. Be able to toggle the tab on and off.

Here is how it's done in the demo outline I posted last time. I have inserted explanatory comment into the code.  We start with setting up some useful constants:


"""A log tab panel for demonstrating plotting techniques."""
log = c.frame.log   # Get the log frame
TABNAME = 'Pyplot'
# We will use these constants when we control the tab later
WIDGET_NAME = f'{TABNAME}-widget'
VISIBLE = f'{TABNAME}-visible'


The core of the code is in the toggle() function:

# The function to toggle the tab on and off.  We use the log's
# contentsDict to hold our own, private variables, since it
# is always going to exist.  These private names are very unlikely
# to be duplicated by any other tab.
def toggle(log):
    """Create, show, or hide pyplot tab in Log pane.
   
    ARGUMENT
    log -- the log panel object for this outline.    
    """

    # Check whether there is a VISIBLE key.  If so,
    # delete the tab and destroy our widget.
    if log.contentsDict.get(VISIBLE, False):
        log.deleteTab(TABNAME)
        log.contentsDict[VISIBLE] = False
        w = log.contentsDict[WIDGET_NAME]
        w.disconnect_all()
        w.deleteLater()
        del(log.contentsDict[WIDGET_NAME])
    else:
        # Create our widget and embed it in a tab
        w = PlotWindow()
        log.createTab(TABNAME, widget = w, createText = False)
        log.selectTab(TABNAME)
        log.contentsDict[VISIBLE] = True
        log.contentsDict[WIDGET_NAME] = w


# If this is the first call of toggle(), create our widget's code
# This will work for (nearly?) any subclass of QWidget.
# Define the widget's class.  It will be instantiated when
# toggle(log) runs.
if not log.contentsDict.get(WIDGET_NAME, None):
    << Main Widget >>

toggle(log)


Next time: how to toggle the tab on and off without deleting our widget.

Thomas Passin

unread,
Apr 16, 2023, 11:51:35 PM4/16/23
to leo-editor
In the last post we saw how to toggle our tab on and off.  But when we turned it off, our deleted our widget instance.  The next time we turned our tab back on, we had to create a new instance of the widget.

Sometimes it is better to keep the widget in existence.  Then we can open a new tab that uses our existing widget instance.  This will maintain the state of our widget, so that we can pick up where we left off.

In the demo outline from the last post, we create a few constant names:

  WIDGET_NAME
  VISIBLE


We stored them in the log frame's contentsDict and used them to check whether our tab was available (open) or not:

# Now you see it
if log.contentsDict.get(VISIBLE, False):
    log.deleteTab(TABNAME)
    # Now you don't
    log.contentsDict[VISIBLE] = False
    log.contentsDict[WIDGET_NAME] = None

log.contentsDict[WIDGET_NAME] stores the widget instance.

To maintain our widget instance while our tab is closed, we need to omit the line

log.contentsDict[WIDGET_NAME] = None

We also need to track whether the widget instance has already been loaded so that we if it has we can just reuse it but if not we can create it.  This constant is named LOADED. We also store it in contentsDict.  Here is the changed code:

log = c.frame.log
TABNAME = 'Pyplot'


VISIBLE = f'{TABNAME}-visible'
LOADED = f'{TABNAME}-loaded'

WIDGET_NAME = f'{TABNAME}-widget'

# If our tab is visible, remove it
# but keep our widget

if log.contentsDict.get(VISIBLE, False):
    log.deleteTab(TABNAME)
    log.contentsDict[VISIBLE] = False
    font = c.config.getColor('font-family')
else:
    # Show our tab, reusing our widget if already loaded
    if log.contentsDict.get(LOADED, False):
        log.createTab(TABNAME,
                      widget = log.contentsDict[WIDGET_NAME],
                      createText = False)
        log.contentsDict[VISIBLE] = True
        log.selectTab(TABNAME)
    else:
        # Create our tab for the first time

        w = PlotWindow()
        log.createTab(TABNAME, widget = w, createText = False)
        log.selectTab(TABNAME)
        log.contentsDict[LOADED] = True

        log.contentsDict[VISIBLE] = True
        log.contentsDict[WIDGET_NAME] = w


Next time we will pass g and c so that we can interact with Leo itself.  We will also show how to change some behavior or state depending on whether our tab is visible or not.  This is a kind of parlor trick, yet can be useful.

Reply all
Reply to author
Forward
0 new messages