wx.Notebook conversion

Working on a project and determined that I would like to use the tab functionality that a wx.Notebook provides. So thinking it would be a simple matter to create a new class and change the object notation to point to my new notebook tabs I began the work. After a day of looking at the code without seeing the tabs I need other eyes on the code.
Why do I still have this blank screen?


The main frame class is working correctly. Inside main frame I’ve created a panel that refers to a class where the objects I would like to display are. In that panel class I’ve create the notebook and referenced the items to it. I believe that another panel class is required because the main frame has one and there can’t be two? Please clarify.
Here is the class that my panel class refers to.

class TabPanel(wx.Panel):
    """
    This class is needed to add the tab panels below. 
    """
    def __init__(self, parent):
      wx.Panel.__init__(self, parent=parent)
class LedgerPanel(wx.Panel):
def __init__(self, parent):
        super().__init__(parent)

panel = wx.Panel(self)
        
        notebook = wx.Notebook(panel, style=wx.NB_TOP)
        tabOne = TabPanel(notebook)
        notebook.AddPage(tabOne, 'tabOne')
        tabOne.SetBackgroundColour((255, 255, 255))

        tabTwo = TabPanel(notebook)
        notebook.AddPage(tabTwo, 'TabTwo')

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
        panel.SetSizer(sizer)
        self.Show()

Here is an example of one object that I think I’m displaying on the notebooks tabOne.

categories = ["Budget Item", "Date", "Credit", "Balance"]
        search_label = wx.StaticText(tabOne, label="Search By:")
        search_label.SetFont(font)
        search_sizer.Add(search_label, 0, wx.ALL, 5)

This is what the program looks like without a notebook for comparison. Just to show that all the other functionality works. The list view is from ObjectListView and ties to a database.


The idea is move what you see in the panel to its own notebook tab. Then I can create other tabs with other content. To overstate the obvious.
What I’m not clear on is the parent child relationship. I think I get it but I can’t defend my understanding in practice. Any thoughts you have are appreciated. I think I’m almost there.

The problem is probably not in the code you posted.
Please post a runnable sample, including the surrounding frame.

You may have another way to get the imports to work but main calls controller, dialogs and model.
All pasted below. I’m happy to provide access the repo if easier. Let me know.

main.py

import controller
import dialogs
import wx.adv
import os
import csv
import wx
from ObjectListView import ObjectListView, ColumnDefn
from wx.lib.wordwrap import wordwrap

# ─────────────────────────────────────────────────────────────────────────────  
# Modification for Notebook
# ─────────────────────────────────────────────────────────────────────────────  

class TabPanel(wx.Panel):
    """
    This class is needed to add the tab panels below. 
    """
    def __init__(self, parent):
      wx.Panel.__init__(self, parent=parent)

class LedgerFrame(wx.Frame):
    """
    The top level frame widget with the Program Title, Program Icon, minimize
    and maximize buttons, the top file menu, bottom status bar
    """
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(
            self,
            None,
            wx.ID_ANY,
            "Account Manager",
            size = (1200, 600)
            )
        
        panel = LedgerPanel(self)
        # panel = wx.Panel(self)
        
        icon = wx.Icon('icon/py1.ico', wx.BITMAP_TYPE_ANY)
        self.SetIcon(icon)
        
        # Add a Status Bar to the Bottom
        statusBar = self.CreateStatusBar()

        self.SetSizeHints(1200, 600)

        # Center the program window on the screen when first opened.
        self.Center()
        
        # ───────────────────────────────────────────────────────────────────────
        # Top MenuBar > File > Edit > View > Tools > Help
        # ───────────────────────────────────────────────────────────────────────
        # First place the MenuBar
        file_menu = wx.MenuBar()
        
        # First MenuBar menu File
        menu1 = wx.Menu()
        file_menu.Append(menu1, "&File")
        
        # File > Open
        fileOpen = menu1.Append(wx.ID_ANY, "&Open", "Open File")
        self.Bind(wx.EVT_MENU, self.onOpen, fileOpen)
        
        # File > New File
        newFile = menu1.Append(wx.ID_ANY, "&New", "New File")
        self.Bind(wx.EVT_MENU, self.onNew, newFile)

        # File > Save File
        onSave = menu1.Append(wx.ID_ANY, "&Save", "Save File")
        self.Bind(wx.EVT_MENU, self.onSave, onSave)
        
        menu1.AppendSeparator() # Thin line separator
        
        # File > Exit (Closes the Program)
        fileExit = menu1.Append(wx.ID_ANY, "&Exit", "Exit File")
        menu1.Bind(wx.EVT_MENU, self.onQuit, fileExit)

        # ───────────────────────────────────────────────────────────────────────
        # Edit Menu
        # ───────────────────────────────────────────────────────────────────────
        menu2 = wx.Menu()
        # Edit Menu
        file_menu.Append(menu2, "&Edit") # Main header "Edit" added last
        # Binding here
        
        # Edit > Copy
        menu2.Append(wx.ID_ANY, "&Copy", "Copy")
        # Binding here
        
        # Edit > Cut
        menu2.Append(wx.ID_ANY, "&Cut", "Cut")
        # Binding here
        
        # Edit > Paste
        menu2.Append(wx.ID_ANY, "Paste", "Paste")
        # Binding here
        
        menu2.AppendSeparator()
        
        # Edit > Options
        menu2.Append(wx.ID_ANY, "&Options...", "Display Options")
        # Binding here
        
        # ───────────────────────────────────────────────────────────────────────
        # View Menu
        # ───────────────────────────────────────────────────────────────────────
        menu3 = wx.Menu()
        
        # View
        file_menu.Append(menu3, "&View") # Main header "View" added last
        
        menu3.Append(wx.ID_ANY, "&Ledger", "Copy")
        # Binding Here
        
        menu3.Append(wx.ID_ANY, "&Budget Items", "Cut")
        # Binding Here
        
        menu3.Append(wx.ID_ANY, "&Accounts", "Cut")
        # Binding Here
        
        menu3.Append(wx.ID_ANY, "&Other", "Paste")
        # Binding Here

        # ───────────────────────────────────────────────────────────────────────
        # Export Menu
        # ───────────────────────────────────────────────────────────────────────
        menu4 = wx.Menu()
        
        # Add Export Menu
        file_menu.Append(menu4, "&Export") # Main header "Tools" added last
        
        # Tools > Export Menu
        exportFile = menu4.Append(wx.ID_ANY, "&Export Ledger", "Export Ledger")
        # Export Binding Here
        self.Bind(wx.EVT_MENU, self.onExport, exportFile)
                
        menu4.Append(wx.ID_ANY, "&Import Database", "Import Ledger")
        # Import Binding Here
        
        menu4.Append(wx.ID_ANY, "Paste", "Paste")
        
        # ───────────────────────────────────────────────────────────────────────
        # Help Menu
        # ───────────────────────────────────────────────────────────────────────
        menu5 = wx.Menu()
        file_menu.Append(menu5, "&Help") # Main header "Edit" added last
        menu5.Append(wx.ID_ANY, "&Copy", "Copy")
        menu5.Append(wx.ID_ANY, "&Cut", "Cut")
        menu5.Append(wx.ID_ANY, "Paste", "Paste")
        
        # Separator
        menu5.AppendSeparator()
        
        aboutModal = menu5.Append(wx.ID_ANY, "&About...", "About")
        menu5.Bind(wx.EVT_MENU, self.onAbout, aboutModal)

        self.SetMenuBar(file_menu)
        self.Show()

    # ───────────────────────────────────────────────────────────────────────
    def onOpen(self, event):
        wx.MessageBox("onOpen event triggered, now add your code ...")
        

    # ───────────────────────────────────────────────────────────────────────
    def onNew(self, event):
        wx.MessageBox("onNew event triggered, now add your code ...")

    # ───────────────────────────────────────────────────────────────────────
    def onSave(self, event):
        wx.MessageBox("onSave event triggered, now add your code ...")
    
    # ───────────────────────────────────────────────────────────────────────
    def onQuit(self, event):
        self.Close()
    
    # ───────────────────────────────────────────────────────────────────────
    def onAbout(self, event):
        info = wx.adv.AboutDialogInfo()
        info.Name = "DataBase Front End"
        info.Version = " v1.0"
        info.Copyright = "(c) 2023"
        info.Description = wordwrap(
            "The \"DataBase Editor\" front end for an SQL DataBase "
            "\nPut some more text here ...",
            350, wx.ClientDC(self))
        info.WebSite = ("http://en.wikipedia.org/wiki/Hello_world", "Hello World home page")
        info.Developers = ["Barny Rubble",
                            "Fred Flintstone"]

        licenseText = "blah " * 250 + "\n\n" + "yadda " * 100
        # ClientDC
        info.License = wordwrap(licenseText, 500, wx.ClientDC(self))

        # Then we call wx.AboutBox giving it that info object
        wx.adv.AboutBox(info)

# ─────────────────────────────────────────────────────────────────────────────
# Export Ledger to CSV. Exports everything mapped below. 
# @ add the ability to choose what fields you want to export. 
# ─────────────────────────────────────────────────────────────────────────────
    def onExport(self, event):
        # wx.MessageBox("onExport event triggered, now add your code ...")
        self.session = controller.connect_to_database()
        self.ledger_results = controller.get_all_records(self.session)
        
        # get all ledger items. 
        records = self.ledger_results
        print("\nwxAccMan.py > Ledger Results")
        for record in records:
            print(f"Export Records: {record.id}, {record.itemDate}, {record.description}")

        # Create csv writer
        with open('export.csv', 'w', newline='') as csvfile:
            csv_writer = csv.writer(csvfile)
            #Column headings for CSV
            csv_writer.writerow(
                ['Date',
                 'Description',
                 'Budget Item',
                 'Credit',
                 'Debit',
                 'Balance'
                ])

        # model fields, must be the correctly spelled field in models.py
            for record in records:
                csv_writer.writerow([
                record.itemDate,
                record.description,
                record.budgetItem,
                record.credit,
                record.debit,
                record.balance,
                ])
            
        return csvfile
# ───── BookFrame Class End ───── #

# ─────────────────────────────────────────────────────────────────────────────
# BookPanel Class > holds the list display. Frame is the parent.
# ─────────────────────────────────────────────────────────────────────────────
class LedgerPanel(wx.Panel):
    """
    The main display of the UI. Displaying the db records. Checks to see if db 
    is present, if not it's created.
    Search By: drop down, search text box. 
    The 4 buttons in the bottom middle under the view panel, Add, Edit, Delete
    and Show All.
    """
# ─────────────────────────────────────────────────────────────────────────────
# The built-in super() (Python 3) function’s primary purpose in wxPython is
# used for referring to the parent class without actually naming it.
# ─────────────────────────────────────────────────────────────────────────────
    def __init__(self, parent):
        super().__init__(parent)


        # ─────────────────────────────────────────────────────────────────────
        # NoteBook
        # ─────────────────────────────────────────────────────────────────────
        # Items we wish to put in the Notebook must be a children of the Notebook
        # panel = LedgerPanel(self)
        panel = wx.Panel(self)
        
        notebook = wx.Notebook(panel, style=wx.NB_TOP)
        tabOne = TabPanel(notebook)
        notebook.AddPage(tabOne, 'tabOne')
        tabOne.SetBackgroundColour((255, 255, 255))

        tabTwo = TabPanel(notebook)
        notebook.AddPage(tabTwo, 'TabTwo')

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
        panel.SetSizer(sizer)
        self.Show()
        
        if not os.path.exists("wxAccMan.db"):
            controller.setup_database()

        self.session = controller.connect_to_database()
        try:
            self.ledger_results = controller.get_all_records(self.session)
            records = self.ledger_results
            for record in records:
                # print(f"Records: {record.itemDate}")
                print(f"Record: {record.itemDate}, {record.description}")
            # print(f"Ledger Results {self.ledger_results}")
            # print("wxAccMan.py > Ledger Results")
        except:
            self.ledger_results = []
    
        # Search widgets sizers
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        search_sizer = wx.BoxSizer(wx.HORIZONTAL)
        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD)
    
        # create the search related widgets
        categories = ["Budget Item", "Date", "Credit", "Balance"]
        search_label = wx.StaticText(tabOne, label="Search By:")
        search_label.SetFont(font)
        search_sizer.Add(search_label, 0, wx.ALL, 5)
    
        # Categories Choices and sizer
        self.categories = wx.ComboBox(tabOne, value="Budget Item", choices=categories)
        search_sizer.Add(self.categories, 0, wx.ALL, 5)
    
        # self.search_ctrl = wx.SearchCtrl(self, style=wx.TE_PROCESS_ENTER)
        self.search_ctrl = wx.TextCtrl(tabOne, style=wx.TE_PROCESS_ENTER)
        self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.search)
        search_sizer.Add(self.search_ctrl, 0, wx.ALL, 5)
    
        self.ledger_results_olv = ObjectListView(tabOne, 
                                style=wx.LC_REPORT | wx.SUNKEN_BORDER)
        
        self.ledger_results_olv.evenRowsBackColor = wx.Colour(230, 230, 230)
        self.ledger_results_olv.oddRowsBackColor = wx.Colour(250, 250, 250)
        self.ledger_results_olv.SetEmptyListMsg("No Records Found")
        
        self.update_ledger_results()

        add_record_btn = wx.Button(tabOne, label="Add")
        add_record_btn.Bind(wx.EVT_BUTTON, self.add_record)
        btn_sizer.Add(add_record_btn, 0, wx.ALL, 5)
    
# ─────────────────────────────────────────────────────────────────────────────
        # Edit Record
        edit_record_btn = wx.Button(tabOne, label="Edit")
        edit_record_btn.Bind(wx.EVT_BUTTON, self.edit_record)
        btn_sizer.Add(edit_record_btn, 0, wx.ALL, 5)
    
# ─────────────────────────────────────────────────────────────────────────────
        # Delete Record
        delete_record_btn = wx.Button(tabOne, label="Delete")
        delete_record_btn.Bind(wx.EVT_BUTTON, self.delete_record)
        btn_sizer.Add(delete_record_btn, 0, wx.ALL, 5)
    
# ─────────────────────────────────────────────────────────────────────────────
        # Show All
        show_all_btn = wx.Button(tabOne, label="Show All")
        show_all_btn.Bind(wx.EVT_BUTTON, self.on_show_all)
        btn_sizer.Add(show_all_btn, 0, wx.ALL, 5)
    
# ─────────────────────────────────────────────────────────────────────────────
        # Add search to main sizer
        main_sizer.Add(search_sizer)
        main_sizer.Add(self.ledger_results_olv, 1, wx.ALL|wx.EXPAND, 5)
        main_sizer.Add(btn_sizer, 0, wx.CENTER)
        self.SetSizer(main_sizer)

        self.Show()

# ─────────────────────────────────────────────────────────────────────────────
    def add_record(self, event):
        """
        Add a record to the database
        """
        os.system('cls')
        print("ADD Record Button Clicked...")
        with dialogs.RecordDialog(self.session) as dlg:
            dlg.ShowModal()
            
        self.show_all_records()
    
# ─────────────────────────────────────────────────────────────────────────────
    def edit_record(self, event):
        """
        Edit a record
        """
        selected_row = self.ledger_results_olv.GetSelectedObject()
        if selected_row is None:
            dialogs.show_message('No row selected!', 'Error')
            return
        
        with dialogs.RecordDialog(self.session,
                                  selected_row, 
                                  title='Modify',
                                  addRecord=False) as dlg:
            dlg.ShowModal()
        
        self.show_all_records()
    
# ─────────────────────────────────────────────────────────────────────────────
    def delete_record(self, event):
        """
        Delete a record
        """
        selected_row = self.ledger_results_olv.GetSelectedObject()
        if selected_row is None:
            dialogs.show_message('No row selected!', 'Error')
            return
        controller.delete_record(self.session, selected_row.id)
        self.show_all_records()
    
    # Called each time a change is made and the display is updated
    def show_all_records(self):
        """
        Updates the record list to show all of them
        """
        self.ledger_results = controller.get_all_records(self.session)
        self.update_ledger_results()
    
# ─────────────────────────────────────────────────────────────────────────────
    def search(self, event):
        """
        Searches database based on the user's filter 
        choice and keyword
        """
        filter_choice = self.categories.GetValue()
        keyword = self.search_ctrl.GetValue()
        self.ledger_results = controller.search_records(self.session,
                                                      filter_choice, keyword)
        self.update_ledger_results()
    
# ─────────────────────────────────────────────────────────────────────────────
    def on_show_all(self, event):
        """
        Updates the record list to show all the records
        """
        self.show_all_records()
    
# ─────────────────────────────────────────────────────────────────────────────
    def update_ledger_results(self):
        """
        Updates the ObjectListView's properties
        https://objectlistview-python-edition.readthedocs.io/en/latest/gettingStarted.html
        """
        # Show the column headers
        # self.book_results_olv.CreateCheckStateColumn()
        self.ledger_results_olv.SetColumns([
                    # Mapped to model.py
                    ColumnDefn("Date", "left", 150, "itemDate"),
                    ColumnDefn("Description", "left", 350, "description"),
                    ColumnDefn("Budget Item", "left", 200, "budgetItem"),
                    ColumnDefn("Credit", "left", 100, "credit"),
                    ColumnDefn("Debit", "left", 100, "debit"),
                    ColumnDefn("Balance", "left", 100, "balance"),
                ])
        # Puts a checkbox column at the first position in the view.
        # the call must be placed after SetColumns() 
        self.ledger_results_olv.CreateCheckStateColumn()
        self.ledger_results_olv.SetObjects(self.ledger_results)

# ───── BookPanel Class End ───── #

if __name__ == "__main__":
    app = wx.App(False)
    frame = LedgerFrame()
    app.MainLoop()

# ─────────────────────────────────── Ξ£ ───────────────────────────────────────

Controller

# ─────────────────────────────────────────────────────────────────────────────
from model import Ledger, OlvLedger
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# ─────────────────────────────────────────────────────────────────────────────
def add_record(session, data):
    """"""
    ledger = Ledger()
    ledger.itemDate = data["ledger"]["itemDate"]
    ledger.description = data["ledger"]["description"]
    ledger.budgetItem = data["ledger"]["budgetItem"]
    ledger.credit = data["ledger"]["credit"]
    ledger.debit = data["ledger"]["debit"]
    ledger.balance = data["ledger"]["balance"]

    print(ledger)
    session.add(ledger)
    session.commit()

def connect_to_database():
    """"""
    engine = create_engine("sqlite:///test.db", echo=True)
    Session = sessionmaker(bind=engine)
    session = Session()

    return session

def convert_results(results):
    """"""
    print(f" Contorller.py convert_results: {results}")
    ledgerItems = []
    for record in results:
        ledger = OlvLedger(record.id, record.itemDate,
                       record.description, record.budgetItem,
                       record.credit, record.debit, record.balance
                       )
        ledger.append(ledgerItems)
        print(f"convert_results: {ledgerItems}")
    return ledgerItems
    
def delete_record(session, id_num):
    """
    Delete a record from the database
    """
    record = session.query(Ledger).filter_by(id=id_num).one()
    session.delete(record)
    session.commit()

# ─────────────────────────────────────────────────────────────────────────────
# look up db row id to edit.
# ─────────────────────────────────────────────────────────────────────────────
def edit_record(session, id_num, row):
    """
    Edit a record
    """
    record = session.query(Ledger).filter_by(id=id_num).one()
    record.itemDate = row["itemDate"]
    record.description = row["description"]
    record.budgetItem = row["budgetItem"]
    record.credit = row["credit"]
    record.debit = row["debit"]
    record.balance = row["balance"]
    session.add(record)
    session.commit()

def get_all_records(session):
    """
    Get all records and return them
    """
    # Get all records in db
    result = session.query(Ledger).all()
    # Pass all records to convert.
    # ledgerItems = convert_results(result)
    # return ledgerItems
    return result

# ─────────────────────────────────────────────────────────────────────────────
def search_records(session, filter_choice, keyword):
    """
    Searches the database based on the filter chosen and the keyword
    given by the user
    """
    if filter_choice == "Budget Item":
        qry = session.query(Ledger)
        result = qry.filter(Ledger.budgetItem.contains('%s' % keyword)).all()
        records = []
        for record in result:
            for record in record.LedgerItems:
                records.append()
        result = records
    elif filter_choice == "Date":
        qry = session.query(Ledger)
        result = qry.filter(Ledger.itemDate.contains('%s' % keyword)).all()
    else:
        qry = session.query(Ledger)
        result = qry.filter(Ledger.debit.contains('%s' % keyword)).all()
    results = convert_results(result)

    return results

def setup_database():
    """"""
    metadata.create_all()

# ─────────────────────────────────── Ξ£ ───────────────────────────────────────

dialogs

import controller
import wx
# ─────────────────────────────────────────────────────────────────────────────
class RecordDialog(wx.Dialog):
    """
    Add / Modify Record dialog
    """
    def __init__(self, session, row=None, title="Add", addRecord=True):
        """Constructor"""
        super().__init__(None, title="%s Record" % title)
        self.addRecord = addRecord
        self.selected_row = row
        self.session = session
        if row:
            itemDate = self.selected_row.itemDate
            description = self.selected_row.description
            budgetItem = self.selected_row.budgetItem
            credit = self.selected_row.credit
            debit = self.selected_row.debit
            balance = self.selected_row.balance
        else:
            itemDate = description = budgetItem = credit = debit = balance = ""

        # Sizers
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)

        # Pop up Dialog size and font variables 
        size = (200, -1)
        font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD)
        
        # Date
        date_lbl = wx.StaticText(self, label="Date:", size=size)
        # date_lbl = wx.DateTime(self, label="Date:", size=size)
        date_lbl.SetFont(font)
        # value comes from the if statement above.
        self.date_txt = wx.TextCtrl(self, value=itemDate, size=(110, -1))
        # self.date_txt = wx.adv.DatePickerCtrl(self, style = wx.adv.DP_SPIN)
        # self.date_txt = wx.adv.DatePickerCtrl(self, style = wx.adv.DP_DROPDOWN)
        
        main_sizer.Add(self.row_builder([date_lbl, self.date_txt]),
                       0, wx.ALL)

# ─────────────────────────────────────────────────────────────────────────────
        # Description
        description_lbl = wx.StaticText(self, label="Description:", size=size)
        description_lbl.SetFont(font)
        self.description_txt = wx.TextCtrl(self, value=description, 
                                           size=(300, -1))
        main_sizer.Add(self.row_builder([description_lbl, self.description_txt]),
                        0, wx.ALL)

        # Budget Item > Move this to a table.
        budgetItem_lbl = wx.StaticText(self, label="Budget Item:", size=size)
        budgetItem_lbl.SetFont(font)
        self.budgetItem_txt = wx.ComboBox(self, size=size, value=budgetItem,
        choices=["AAA Registration Fee",
        "Administration",
        "Apparel",
        "Athletic Therapist",
        "Banking Fee's",
        "Bus (Playoffs)",
        "Bus (Tournament)",
        "Cash Calendars",
        "Checks",
        "Deposit",
        "Expense Reimbursement",
        "Fundraising",
        "Game Ice (Regular Season)",
        "Game Officials (Playoffs)",
        "Game Officials (Regular Season)",
        "Gate Profit (Playoffs)",
        "Gate Profit (Regular Season)",
        "Goal Tending Coach",
        "Hotels (Playoffs)",
        "Hotels (Regular Season)",
        "Hotels (Tournaments)",
        "League Fee's",
        "Meals (Playoffs)",
        "Meals (Regular Season)",
        "Meals (Tournaments)",
        "Medical Supplies",
        "Misc.",
        "Misc. (Tournament)",
        "MMHL Playoff Assessments",
        "Other Income",
        "Player Fees",
        "Playoff Game Ice",
        "Practice Ice (Playoffs)",
        "Practice Ice (Regular Season)",
        "Practice Jerseys/Socks",
        "Pre-Season",
        "Team Sponsorships",
        "Team Supplies",
        "Tournament Fees",
        "Tournament",
        "Tournament Flights",
        "50/50 (Playoffs)",
        "50/50 (Regular Season)",
        ])
        
        main_sizer.Add(self.row_builder([budgetItem_lbl, self.budgetItem_txt]),
                        0, wx.ALL)
# ─────────────────────────────────────────────────────────────────────────────
        # Credit
        credit_lbl = wx.StaticText(self, label="Credit:", size=size)
        credit_lbl.SetFont(font)
        self.credit_txt = wx.TextCtrl(self, value=credit)
        main_sizer.Add(self.row_builder([credit_lbl, self.credit_txt]), 
                       0, wx.ALL)

# ─────────────────────────────────────────────────────────────────────────────
        #Debit
        debit_lbl = wx.StaticText(self, label="Debit:", size=size)
        debit_lbl.SetFont(font)
        self.debit_txt = wx.TextCtrl(self, value=debit)
        main_sizer.Add(self.row_builder([debit_lbl, self.debit_txt]),
                       0, wx.ALL)

# ─────────────────────────────────────────────────────────────────────────────
        # Balance
        balance_lbl = wx.StaticText(self, label="Balance:", size=size)
        balance_lbl.SetFont(font)
        self.balance_txt = wx.TextCtrl(self, value=balance)
        main_sizer.Add(self.row_builder([balance_lbl, self.balance_txt]),
                       0, wx.ALL)

# ─────────────────────────────────────────────────────────────────────────────
        # Add Record Button bound to on_record()
        ok_btn = wx.Button(self, label="%s Record" % title)
        ok_btn.Bind(wx.EVT_BUTTON, self.on_record)
        btn_sizer.Add(ok_btn, 0, wx.ALL, 5)
        
        # Close Button
        cancel_btn = wx.Button(self, label="Close")
        cancel_btn.Bind(wx.EVT_BUTTON, self.on_close)
        btn_sizer.Add(cancel_btn, 0, wx.ALL, 5)

        main_sizer.Add(btn_sizer, 0, wx.CENTER)
        self.SetSizerAndFit(main_sizer)

    def get_data(self):
        """
        Gets the data from the widgets in the dialog
        Also display an error message if required fields
        are empty
        """
        ledger_dict = {}

        # Get the user entered text and store it in each variable.
        itemDate = self.date_txt.GetValue()
        description = self.description_txt.GetValue()
        budgetItem = self.budgetItem_txt.GetValue()
        credit = self.credit_txt.GetValue()
        debit = self.debit_txt.GetValue()
        balance = self.balance_txt.GetValue()
        
        # Add the user entered data to ledger_dict.
        ledger_dict["itemDate"] = itemDate
        ledger_dict["description"] = description
        ledger_dict["budgetItem"] = budgetItem
        ledger_dict["credit"] = credit
        ledger_dict["debit"] = debit
        ledger_dict["balance"] = balance

        # if itemDate == "":
        #     show_message("Date is required !",
        #                  "Error")
        #     return None

        return ledger_dict

    def on_add(self):
        """
        Add the record to the database
        """
        ledger_dict = self.get_data()
        if ledger_dict is None:
            print("Nothing in get_data")
            return

        data = ({"ledger":ledger_dict})
        controller.add_record(self.session, data)
        print(f'on_add > {ledger_dict}')

        # show dialog upon completion
        show_message("Ledger Item Added",
                     "Success!", wx.ICON_INFORMATION)

        # clear dialog so we can add another record
        for child in self.GetChildren():
            if isinstance(child, wx.TextCtrl):
                child.SetValue("")

# ─────────────────────────────────────────────────────────────────────────────
    def on_close(self, event):
        """
        Close the dialog
        """
        self.Close()

# ─────────────────────────────────────────────────────────────────────────────
    def on_edit(self):
        """
        Edit a record in the database
        """
        ledger_dict = self.get_data()
        controller.edit_record(self.session, self.selected_row.id, ledger_dict)
        show_message("Ledger Edited Successfully!", "Success",
                       wx.ICON_INFORMATION)
        self.Close()

# ─────────────────────────────────────────────────────────────────────────────
    # Bound to Add and Edit Buttons > append 
    def on_record(self, event):
        """
        Add or edit a record
        """
        if self.addRecord:
            self.on_add()
        else:
            self.on_edit()
        self.date_txt.SetFocus()

# ─────────────────────────────────────────────────────────────────────────────
# Build a row of widgets.
# ─────────────────────────────────────────────────────────────────────────────
    def row_builder(self, widgets):
        """
        Helper function for building a row of widgets
        """
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        lbl, txt = widgets
        # txt = widgets
        sizer.Add(lbl, 0, wx.ALL, 5)
        sizer.Add(txt, 1, wx.ALL, 5)
        return sizer
# ─────────────────────────────────────────────────────────────────────────────
# End of Class RecordDialog(wx.Dialog)


# ─────────────────────────────────────────────────────────────────────────────
# Called when required information is missing
# ─────────────────────────────────────────────────────────────────────────────
def show_message(message, caption, flag=wx.ICON_ERROR):
    """
    Show a message dialog
    """
    msg = wx.MessageDialog(None, message=message,
                           caption=caption, style=flag)
    msg.ShowModal()
    msg.Destroy()


if __name__ == "__main__":
    app = wx.App(False)
    dlg = RecordDialog(session=None)
    dlg.ShowModal()
    dlg.Destroy()
    app.MainLoop()

model

from sqlalchemy import Table, Column, create_engine
from sqlalchemy import Integer, ForeignKey, Date, String, Unicode, CHAR
import re
# from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relation, sessionmaker
from datetime import date
# ─────────────────────────────────────────────────────────────────────────────
# disable echo=True for release. echo=True sends the SQL commands to stdout so
# you can see what's happening. Not needed for a final product.
engine = create_engine("sqlite:///test.db", echo=True)
Base = declarative_base()
metadata = Base.metadata
print(engine.table_names())

class OlvLedger(object):
    """
    Ledger model (Object) for ObjectListView in main
    """

    def __init__(self, id, itemDate, description, budgetItem, credit, 
                 debit, balance):
   
        self.id = id  # unique row id from database
        self.ItemDate = itemDate
        self.description = description
        self.budgetItem = budgetItem
        self.credit = credit
        self.debit = debit
        self.balance = balance

class Ledger(Base):
    """"""
    __tablename__ = "ledger"

    id = Column(Integer, primary_key=True)
    itemDate = Column("Date", CHAR)
    description = Column("Description", CHAR)
    budgetItem = Column("Budget Item", CHAR)
    credit = Column("Credit", CHAR)
    debit = Column("Debit", CHAR)
    balance = Column("Balance", CHAR)

metadata.create_all(engine)

# ─────────────────────────────────── Ξ£ ───────────────────────────────────────

Well, a runnable sample is something completely different: it’s a minimal sample that other people can run and which demonstrates the problem.
I doubt that anyone wants to make your code running on his/her machine with all the dependencies.

I think this code does roughly what you want:
wxglade_out.py (1.4 KB)

P.S.: HOWTO: Get help with a small runnable sample

My apologies, I had considered what I was posting was a little too much. There are dependencies in the main program that will not run without them. I’ll take another stab at it and see if I can do better.
Thanks @DietmarSchwertberger.

The wxglade_out.py run correctly. I have my own version of that. My problem is β€œshoe horning” all my existing code into a tab. I think the question I’m posing is larger than I thought.

Why do you have a panel on a panel if you just add a notebook to it?

class LedgerPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        panel = wx.Panel(self)
        notebook = wx.Notebook(panel, style=wx.NB_TOP)

Try this:

class LedgerPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        notebook = wx.Notebook(self, style=wx.NB_TOP)

Actually, if you don’t place anything above or beneath the notebook, you can just add the notebook to the frame instead of using the panel and the sizer with just one item.
wxglade_out2.py (1.2 KB)

You have created

panel = wx.Panel(self)

under LedgerPanel class, but you didn’t add it to main_sizer. You should do something like this:

        main_sizer.Add(search_sizer)
        main_sizer.Add(panel, 1, wx.ALL | wx.EXPAND, 5)
        main_sizer.Add(btn_sizer, 0, wx.CENTER)
        self.SetSizer(main_sizer)
1 Like

@steve2 ! Thanks so much. One line out!
Here’s what it looks like so far, it’s on the right track.

Here’s what the other missing items look like.

Any idea what sizer is responsible for the notebook being placed where it is?
image

main_sizer.Add(search_sizer)
main_sizer.Add(self.ledger_results_olv, 1, wx.ALL|wx.EXPAND, 5)
main_sizer.Add(btn_sizer, 0, wx.CENTER)
main_sizer.Add(panel, 1, wx.ALL | wx.EXPAND, 5)
self.SetSizer(main_sizer)

And this …

main_sizer = wx.BoxSizer(wx.VERTICAL)
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)

Hi,

Here is how I did it, best regards:

import wx
import wx.lib.agw.flatnotebook as fnb


class TheFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "The Frame",
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX
                          )
        self.create_menu_bar()
        self.create_panel()
        self.create_search_bar()
        self.create_notebook()
        self.Layout()
    def create_panel(self):
        self.main_panel = wx.Panel(self, -1)
        self.main_panel_sizer = wx.BoxSizer(wx.VERTICAL)
        self.main_panel.SetSizer(self.main_panel_sizer)

    def create_search_bar(self):
        search_sizer = wx.BoxSizer(wx.HORIZONTAL)
        search_font = wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, faceName="Calibri")
        search_label = wx.StaticText(self.main_panel, -1, "Search By: ")
        search_label.SetFont(search_font)

        sampleList = ['one', 'two', 'three']
        self.ch = wx.Choice(self.main_panel, -1, (100, 50), choices=sampleList)

        self.text_ctrl = wx.TextCtrl(self.main_panel, -1)

        search_sizer.Add(search_label, 0, wx.ALL, 5)
        search_sizer.Add(self.ch, 0, wx.ALL, 5)
        search_sizer.Add(self.text_ctrl, 0, wx.ALL, 5)

        self.main_panel_sizer.Add(search_sizer, 1, wx.ALL, 5)

    def create_notebook(self):
        self.the_notebook = fnb.FlatNotebook(self.main_panel, -1, agwStyle=fnb.FNB_VC71 | fnb.FNB_NO_X_BUTTON)
        self.notebook_tab_1 =  wx.Panel(self.main_panel, -1)
        self.notebook_tab_2 =  wx.Panel(self.main_panel, -1)
        self.the_notebook.AddPage(self.notebook_tab_1, "Tab 1")
        self.the_notebook.AddPage(self.notebook_tab_2, "Tab 2")
        self.list_ctrl_1 = wx.ListCtrl(self.notebook_tab_1, -1, style=wx.LC_REPORT)
        self.list_ctrl_2 = wx.ListCtrl(self.notebook_tab_2, -1, style=wx.LC_REPORT)

        tab_1_sizer = wx.BoxSizer(wx.VERTICAL)
        tab_2_sizer = wx.BoxSizer(wx.VERTICAL)
        tab_1_sizer.Add(self.list_ctrl_1, 1, wx.EXPAND)
        tab_2_sizer.Add(self.list_ctrl_2, 1, wx.EXPAND)


        self.list_ctrl_1.InsertColumn(0, "Col 1")
        self.list_ctrl_1.InsertColumn(1, "Col 2")
        self.list_ctrl_1.InsertColumn(2, "Col 3")

        self.list_ctrl_2.InsertColumn(0, "Col 1")
        self.list_ctrl_2.InsertColumn(1, "Col 2")
        self.list_ctrl_2.InsertColumn(2, "Col 3")

        for ix, list_ctrl in enumerate([self.list_ctrl_1, self.list_ctrl_2]):
            for i in range(5):
                index = list_ctrl.InsertItem(list_ctrl.GetItemCount(), f"Test {str(i*1*(ix+1))}")
                list_ctrl.SetItem(index, 1, f"Test {str(i*2*(ix+1))}")
                list_ctrl.SetItem(index, 2, f"Test {str(i*3*(ix+1))}")

        self.notebook_tab_1.SetSizer(tab_1_sizer)
        self.notebook_tab_2.SetSizer(tab_2_sizer)
        self.notebook_tab_1.Layout()
        self.notebook_tab_2.Layout()

        self.main_panel_sizer.Add(self.the_notebook, 1, wx.EXPAND|wx.ALL, 10)
    def create_menu_bar(self):
        self.menuBar = wx.MenuBar()
        self.menu_file = wx.Menu()
        self.menu_help = wx.Menu()
        self.menuBar.Append(self.menu_file, "File")
        self.menuBar.Append(self.menu_help, "Help")
        self.SetMenuBar(self.menuBar)

if __name__ == "__main__":
    app = wx.App(False)
    frame = TheFrame()
    frame.Show()
    app.MainLoop()

The behavior is the same.

I’m wondering if this is the β€œcorrect” behavior:
What I would like is for the notebook to fill the entire panel space and in turn the panel fill the frame. I can’t find the correct combination of sizers, so far.

notebook = wx.Notebook(panel, style=wx.NB_TOP)
tabOne = TabPanel(notebook)
notebook.AddPage(tabOne, 'tabOne')
notebook_sizer = wx.BoxSizer(wx.VERTICAL)
notebook_sizer.Add(notebook, 1, wx.ALL | wx.EXPAND, 10)
main_sizer.Add(panel, 1, wx.ALL | wx.EXPAND, 5)

there are more than just box sizers & windows can be split or parts hidden :wink:
this is a browser over big data (the left side dead, for search etc) for playing :partying_face:
ChuckM_scrwin_blth_refactored.py (1.6 KB)
scrwin_pnl_win_fgs.py (12.3 KB)

Hi,

Replace:

self.main_panel_sizer.Add(search_sizer, 1, wx.ALL, 5)

with:

self.main_panel_sizer.Add(search_sizer, 0, wx.ALL, 5)

So that the notebook expands vertically on panel:

import wx
import wx.lib.agw.flatnotebook as fnb


class TheFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "The Frame", size=(800, 600),
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX
                          )
        self.create_menu_bar()
        self.create_panel()
        self.create_search_bar()
        self.create_notebook()
        self.Layout()

    def create_panel(self):
        self.main_panel = wx.Panel(self, -1)
        self.main_panel_sizer = wx.BoxSizer(wx.VERTICAL)
        self.main_panel.SetSizer(self.main_panel_sizer)

    def create_search_bar(self):
        search_sizer = wx.BoxSizer(wx.HORIZONTAL)
        search_font = wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, faceName="Calibri")
        search_label = wx.StaticText(self.main_panel, -1, "Search By: ")
        search_label.SetFont(search_font)

        sampleList = ['one', 'two', 'three']
        self.ch = wx.Choice(self.main_panel, -1, (100, 50), choices=sampleList)

        self.text_ctrl = wx.TextCtrl(self.main_panel, -1)

        search_sizer.Add(search_label, 0, wx.ALL, 5)
        search_sizer.Add(self.ch, 0, wx.ALL, 5)
        search_sizer.Add(self.text_ctrl, 0, wx.ALL, 5)

        self.main_panel_sizer.Add(search_sizer, 0, wx.ALL, 5)

    def create_notebook(self):
        self.the_notebook = fnb.FlatNotebook(self.main_panel, -1, agwStyle=fnb.FNB_VC71 | fnb.FNB_NO_X_BUTTON)
        self.notebook_tab_1 =  wx.Panel(self.main_panel, -1)
        self.notebook_tab_2 =  wx.Panel(self.main_panel, -1)
        self.the_notebook.AddPage(self.notebook_tab_1, "Tab 1")
        self.the_notebook.AddPage(self.notebook_tab_2, "Tab 2")
        self.list_ctrl_1 = wx.ListCtrl(self.notebook_tab_1, -1, style=wx.LC_REPORT)
        self.list_ctrl_2 = wx.ListCtrl(self.notebook_tab_2, -1, style=wx.LC_REPORT)

        tab_1_sizer = wx.BoxSizer(wx.VERTICAL)
        tab_2_sizer = wx.BoxSizer(wx.VERTICAL)
        tab_1_sizer.Add(self.list_ctrl_1, 1, wx.EXPAND)
        tab_2_sizer.Add(self.list_ctrl_2, 1, wx.EXPAND)


        self.list_ctrl_1.InsertColumn(0, "Col 1")
        self.list_ctrl_1.InsertColumn(1, "Col 2")
        self.list_ctrl_1.InsertColumn(2, "Col 3")

        self.list_ctrl_2.InsertColumn(0, "Col 1")
        self.list_ctrl_2.InsertColumn(1, "Col 2")
        self.list_ctrl_2.InsertColumn(2, "Col 3")

        for ix, list_ctrl in enumerate([self.list_ctrl_1, self.list_ctrl_2]):
            for i in range(5):
                index = list_ctrl.InsertItem(list_ctrl.GetItemCount(), f"Test {str(i*1*(ix+1))}")
                list_ctrl.SetItem(index, 1, f"Test {str(i*2*(ix+1))}")
                list_ctrl.SetItem(index, 2, f"Test {str(i*3*(ix+1))}")

        self.notebook_tab_1.SetSizer(tab_1_sizer)
        self.notebook_tab_2.SetSizer(tab_2_sizer)
        self.notebook_tab_1.Layout()
        self.notebook_tab_2.Layout()

        self.main_panel_sizer.Add(self.the_notebook, 1, wx.EXPAND|wx.ALL, 10)

    def create_menu_bar(self):
        self.menuBar = wx.MenuBar()
        self.menu_file = wx.Menu()
        self.menu_help = wx.Menu()
        self.menuBar.Append(self.menu_file, "File")
        self.menuBar.Append(self.menu_help, "Help")
        self.SetMenuBar(self.menuBar)

if __name__ == "__main__":
    app = wx.App(False)
    frame = TheFrame()
    frame.Show()
    app.MainLoop()

Sorry to jump into the thread.

Is there a way to make a Notebook expand/contract its vertical size based the height of only the visible tab.
The notebook vertical size seems to be derived from the β€œmax” of all of the added tabs, including the hidden ones.
Is there an β€œoption” or β€œflag” available to do this?

Not sure if this will answer your question @Gabbo or just create new ones but here goes…

Unfortunately this thread evolved from an SLQAlchemy question. This subject should be in it’s own thread on GUI. Not sure how I would do this or if its possible. Be that as it may, my problem with the GUI was resolved by using wxGlade as suggested by @RichardT. Now that I know better I’ll be starting all my complicated GUI’s using wxGlade first. I found coding with three zones too much to follow in the code alone.
Further complicating the issue was my misunderstanding of β€œpanel”. Panel was instantiated in the frame class. In that class I had created the various frame widgets. I then create another class that containing the wxNotebook, instantiated and added to a sizer. That sizer is how to get the control I was after. Filling the entire space of the panel.

panel_sizer = wx.BoxSizer(wx.VERTICAL)

Notebook instantiation, 1 = expand in the horizontal direction filling all the space, 5 = border of 5 pixels on all sides.

notebook = wx.Notebook(self, wx.ID_ANY)
panel_sizer.Add(notebook, 1, wx.ALL | wx.EXPAND , 5)

add the β€œtab” aka page to the notebook, give the tab a name

tabOne = wx.Panel(notebook)
notebook.AddPage(tabOne, 'Ledger') 

Create a sizer for the notebook. This is so that I could add all the widgets into the tab of the notebook.
Critical for my project and were all the confusion begins.

tabOne_sizer = wx.BoxSizer(wx.VERTICAL)

Lastly set the sizers

tabOne.SetSizer(tabOne_sizer)
self.SetSizer(panel_sizer)
self.Layout()
self.Centre()

Hope that helps.

I found a solution from several other posts in the group regarding resizing Notebooks.
The Notebook β€œminimum” size is always the maximum of the β€œBestSize” of all of its tabs.

I found a solution by resizing the Notebook manually when the event EVT_NOTEBOOK_PAGE_CHANGED is triggered.

    def ResizeTabs(self, event):
        currentPage = self.config_nb.GetCurrentPage()
        currentPageHeight = currentPage.GetBestSize().GetHeight() + 28
        self.config_nb.SetMinClientSize((-1, currentPageHeight))
        self.Layout()

This works as I wanted & the rest of the Frame expands or contracts when the Notebook changes height.

I always have a 28px offset between the β€œbestHeight” and the actual minimum height which I can’t figure out where it comes from, but it works consistently with this offset even if the code is not pretty to look at…